Merge pull request #6862 from coollabsio/andrasbacsai/livewire-model-binding
Complete Livewire legacy model binding migration (25+ components)
This commit is contained in:
commit
dab30da63c
49 changed files with 1426 additions and 555 deletions
35
app/Livewire/Concerns/SynchronizesModelData.php
Normal file
35
app/Livewire/Concerns/SynchronizesModelData.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Concerns;
|
||||||
|
|
||||||
|
trait SynchronizesModelData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the mapping between component properties and model keys.
|
||||||
|
*
|
||||||
|
* @return array<string, string> Array mapping property names to model keys (e.g., ['content' => 'fileStorage.content'])
|
||||||
|
*/
|
||||||
|
abstract protected function getModelBindings(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize component properties TO the model.
|
||||||
|
* Copies values from component properties to the model.
|
||||||
|
*/
|
||||||
|
protected function syncToModel(): void
|
||||||
|
{
|
||||||
|
foreach ($this->getModelBindings() as $property => $modelKey) {
|
||||||
|
data_set($this, $modelKey, $this->{$property});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize component properties FROM the model.
|
||||||
|
* Copies values from the model to component properties.
|
||||||
|
*/
|
||||||
|
protected function syncFromModel(): void
|
||||||
|
{
|
||||||
|
foreach ($this->getModelBindings() as $property => $modelKey) {
|
||||||
|
$this->{$property} = data_get($this, $modelKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ public function __construct(
|
||||||
public bool $readonly,
|
public bool $readonly,
|
||||||
public bool $allowTab,
|
public bool $allowTab,
|
||||||
public bool $spellcheck,
|
public bool $spellcheck,
|
||||||
public bool $autofocus = false,
|
public bool $autofocus,
|
||||||
public ?string $helper,
|
public ?string $helper,
|
||||||
public bool $realtimeValidation,
|
public bool $realtimeValidation,
|
||||||
public bool $allowToPeak,
|
public bool $allowToPeak,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Livewire\Project\Application;
|
namespace App\Livewire\Project\Application;
|
||||||
|
|
||||||
use App\Actions\Application\GenerateConfig;
|
use App\Actions\Application\GenerateConfig;
|
||||||
|
use App\Livewire\Concerns\SynchronizesModelData;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Support\ValidationPatterns;
|
use App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
class General extends Component
|
class General extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests;
|
use AuthorizesRequests;
|
||||||
|
use SynchronizesModelData;
|
||||||
|
|
||||||
public string $applicationId;
|
public string $applicationId;
|
||||||
|
|
||||||
|
|
@ -23,6 +25,8 @@ class General extends Component
|
||||||
|
|
||||||
public string $name;
|
public string $name;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
public ?string $fqdn = null;
|
public ?string $fqdn = null;
|
||||||
|
|
||||||
public string $git_repository;
|
public string $git_repository;
|
||||||
|
|
@ -31,14 +35,82 @@ class General extends Component
|
||||||
|
|
||||||
public ?string $git_commit_sha = null;
|
public ?string $git_commit_sha = null;
|
||||||
|
|
||||||
|
public ?string $install_command = null;
|
||||||
|
|
||||||
|
public ?string $build_command = null;
|
||||||
|
|
||||||
|
public ?string $start_command = null;
|
||||||
|
|
||||||
public string $build_pack;
|
public string $build_pack;
|
||||||
|
|
||||||
|
public string $static_image;
|
||||||
|
|
||||||
|
public string $base_directory;
|
||||||
|
|
||||||
|
public ?string $publish_directory = null;
|
||||||
|
|
||||||
public ?string $ports_exposes = null;
|
public ?string $ports_exposes = null;
|
||||||
|
|
||||||
|
public ?string $ports_mappings = null;
|
||||||
|
|
||||||
|
public ?string $custom_network_aliases = null;
|
||||||
|
|
||||||
|
public ?string $dockerfile = null;
|
||||||
|
|
||||||
|
public ?string $dockerfile_location = null;
|
||||||
|
|
||||||
|
public ?string $dockerfile_target_build = null;
|
||||||
|
|
||||||
|
public ?string $docker_registry_image_name = null;
|
||||||
|
|
||||||
|
public ?string $docker_registry_image_tag = null;
|
||||||
|
|
||||||
|
public ?string $docker_compose_location = null;
|
||||||
|
|
||||||
|
public ?string $docker_compose = null;
|
||||||
|
|
||||||
|
public ?string $docker_compose_raw = null;
|
||||||
|
|
||||||
|
public ?string $docker_compose_custom_start_command = null;
|
||||||
|
|
||||||
|
public ?string $docker_compose_custom_build_command = null;
|
||||||
|
|
||||||
|
public ?string $custom_labels = null;
|
||||||
|
|
||||||
|
public ?string $custom_docker_run_options = null;
|
||||||
|
|
||||||
|
public ?string $pre_deployment_command = null;
|
||||||
|
|
||||||
|
public ?string $pre_deployment_command_container = null;
|
||||||
|
|
||||||
|
public ?string $post_deployment_command = null;
|
||||||
|
|
||||||
|
public ?string $post_deployment_command_container = null;
|
||||||
|
|
||||||
|
public ?string $custom_nginx_configuration = null;
|
||||||
|
|
||||||
|
public bool $is_static = false;
|
||||||
|
|
||||||
|
public bool $is_spa = false;
|
||||||
|
|
||||||
|
public bool $is_build_server_enabled = false;
|
||||||
|
|
||||||
public bool $is_preserve_repository_enabled = false;
|
public bool $is_preserve_repository_enabled = false;
|
||||||
|
|
||||||
public bool $is_container_label_escape_enabled = true;
|
public bool $is_container_label_escape_enabled = true;
|
||||||
|
|
||||||
|
public bool $is_container_label_readonly_enabled = false;
|
||||||
|
|
||||||
|
public bool $is_http_basic_auth_enabled = false;
|
||||||
|
|
||||||
|
public ?string $http_basic_auth_username = null;
|
||||||
|
|
||||||
|
public ?string $http_basic_auth_password = null;
|
||||||
|
|
||||||
|
public ?string $watch_paths = null;
|
||||||
|
|
||||||
|
public string $redirect;
|
||||||
|
|
||||||
public $customLabels;
|
public $customLabels;
|
||||||
|
|
||||||
public bool $labelsChanged = false;
|
public bool $labelsChanged = false;
|
||||||
|
|
@ -66,50 +138,50 @@ class General extends Component
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'application.name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'application.description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'application.fqdn' => 'nullable',
|
'fqdn' => 'nullable',
|
||||||
'application.git_repository' => 'required',
|
'git_repository' => 'required',
|
||||||
'application.git_branch' => 'required',
|
'git_branch' => 'required',
|
||||||
'application.git_commit_sha' => 'nullable',
|
'git_commit_sha' => 'nullable',
|
||||||
'application.install_command' => 'nullable',
|
'install_command' => 'nullable',
|
||||||
'application.build_command' => 'nullable',
|
'build_command' => 'nullable',
|
||||||
'application.start_command' => 'nullable',
|
'start_command' => 'nullable',
|
||||||
'application.build_pack' => 'required',
|
'build_pack' => 'required',
|
||||||
'application.static_image' => 'required',
|
'static_image' => 'required',
|
||||||
'application.base_directory' => 'required',
|
'base_directory' => 'required',
|
||||||
'application.publish_directory' => 'nullable',
|
'publish_directory' => 'nullable',
|
||||||
'application.ports_exposes' => 'required',
|
'ports_exposes' => 'required',
|
||||||
'application.ports_mappings' => 'nullable',
|
'ports_mappings' => 'nullable',
|
||||||
'application.custom_network_aliases' => 'nullable',
|
'custom_network_aliases' => 'nullable',
|
||||||
'application.dockerfile' => 'nullable',
|
'dockerfile' => 'nullable',
|
||||||
'application.docker_registry_image_name' => 'nullable',
|
'docker_registry_image_name' => 'nullable',
|
||||||
'application.docker_registry_image_tag' => 'nullable',
|
'docker_registry_image_tag' => 'nullable',
|
||||||
'application.dockerfile_location' => 'nullable',
|
'dockerfile_location' => 'nullable',
|
||||||
'application.docker_compose_location' => 'nullable',
|
'docker_compose_location' => 'nullable',
|
||||||
'application.docker_compose' => 'nullable',
|
'docker_compose' => 'nullable',
|
||||||
'application.docker_compose_raw' => 'nullable',
|
'docker_compose_raw' => 'nullable',
|
||||||
'application.dockerfile_target_build' => 'nullable',
|
'dockerfile_target_build' => 'nullable',
|
||||||
'application.docker_compose_custom_start_command' => 'nullable',
|
'docker_compose_custom_start_command' => 'nullable',
|
||||||
'application.docker_compose_custom_build_command' => 'nullable',
|
'docker_compose_custom_build_command' => 'nullable',
|
||||||
'application.custom_labels' => 'nullable',
|
'custom_labels' => 'nullable',
|
||||||
'application.custom_docker_run_options' => 'nullable',
|
'custom_docker_run_options' => 'nullable',
|
||||||
'application.pre_deployment_command' => 'nullable',
|
'pre_deployment_command' => 'nullable',
|
||||||
'application.pre_deployment_command_container' => 'nullable',
|
'pre_deployment_command_container' => 'nullable',
|
||||||
'application.post_deployment_command' => 'nullable',
|
'post_deployment_command' => 'nullable',
|
||||||
'application.post_deployment_command_container' => 'nullable',
|
'post_deployment_command_container' => 'nullable',
|
||||||
'application.custom_nginx_configuration' => 'nullable',
|
'custom_nginx_configuration' => 'nullable',
|
||||||
'application.settings.is_static' => 'boolean|required',
|
'is_static' => 'boolean|required',
|
||||||
'application.settings.is_spa' => 'boolean|required',
|
'is_spa' => 'boolean|required',
|
||||||
'application.settings.is_build_server_enabled' => 'boolean|required',
|
'is_build_server_enabled' => 'boolean|required',
|
||||||
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
|
'is_container_label_escape_enabled' => 'boolean|required',
|
||||||
'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
|
'is_container_label_readonly_enabled' => 'boolean|required',
|
||||||
'application.settings.is_preserve_repository_enabled' => 'boolean|required',
|
'is_preserve_repository_enabled' => 'boolean|required',
|
||||||
'application.is_http_basic_auth_enabled' => 'boolean|required',
|
'is_http_basic_auth_enabled' => 'boolean|required',
|
||||||
'application.http_basic_auth_username' => 'string|nullable',
|
'http_basic_auth_username' => 'string|nullable',
|
||||||
'application.http_basic_auth_password' => 'string|nullable',
|
'http_basic_auth_password' => 'string|nullable',
|
||||||
'application.watch_paths' => 'nullable',
|
'watch_paths' => 'nullable',
|
||||||
'application.redirect' => 'string|required',
|
'redirect' => 'string|required',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,31 +190,31 @@ protected function messages(): array
|
||||||
return array_merge(
|
return array_merge(
|
||||||
ValidationPatterns::combinedMessages(),
|
ValidationPatterns::combinedMessages(),
|
||||||
[
|
[
|
||||||
'application.name.required' => 'The Name field is required.',
|
'name.required' => 'The Name field is required.',
|
||||||
'application.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||||
'application.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||||
'application.git_repository.required' => 'The Git Repository field is required.',
|
'git_repository.required' => 'The Git Repository field is required.',
|
||||||
'application.git_branch.required' => 'The Git Branch field is required.',
|
'git_branch.required' => 'The Git Branch field is required.',
|
||||||
'application.build_pack.required' => 'The Build Pack field is required.',
|
'build_pack.required' => 'The Build Pack field is required.',
|
||||||
'application.static_image.required' => 'The Static Image field is required.',
|
'static_image.required' => 'The Static Image field is required.',
|
||||||
'application.base_directory.required' => 'The Base Directory field is required.',
|
'base_directory.required' => 'The Base Directory field is required.',
|
||||||
'application.ports_exposes.required' => 'The Exposed Ports field is required.',
|
'ports_exposes.required' => 'The Exposed Ports field is required.',
|
||||||
'application.settings.is_static.required' => 'The Static setting is required.',
|
'is_static.required' => 'The Static setting is required.',
|
||||||
'application.settings.is_static.boolean' => 'The Static setting must be true or false.',
|
'is_static.boolean' => 'The Static setting must be true or false.',
|
||||||
'application.settings.is_spa.required' => 'The SPA setting is required.',
|
'is_spa.required' => 'The SPA setting is required.',
|
||||||
'application.settings.is_spa.boolean' => 'The SPA setting must be true or false.',
|
'is_spa.boolean' => 'The SPA setting must be true or false.',
|
||||||
'application.settings.is_build_server_enabled.required' => 'The Build Server setting is required.',
|
'is_build_server_enabled.required' => 'The Build Server setting is required.',
|
||||||
'application.settings.is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
|
'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
|
||||||
'application.settings.is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
|
'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
|
||||||
'application.settings.is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||||
'application.settings.is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
|
'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
|
||||||
'application.settings.is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||||
'application.settings.is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
|
'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
|
||||||
'application.settings.is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||||
'application.is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
|
'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||||
'application.is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||||
'application.redirect.required' => 'The Redirect setting is required.',
|
'redirect.required' => 'The Redirect setting is required.',
|
||||||
'application.redirect.string' => 'The Redirect setting must be a string.',
|
'redirect.string' => 'The Redirect setting must be a string.',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -193,11 +265,15 @@ public function mount()
|
||||||
$this->parsedServices = $this->application->parse();
|
$this->parsedServices = $this->application->parse();
|
||||||
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
|
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
|
||||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||||
|
// Still sync data even if parse fails, so form fields are populated
|
||||||
|
$this->syncFromModel();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->dispatch('error', $e->getMessage());
|
$this->dispatch('error', $e->getMessage());
|
||||||
|
// Still sync data even on error, so form fields are populated
|
||||||
|
$this->syncFromModel();
|
||||||
}
|
}
|
||||||
if ($this->application->build_pack === 'dockercompose') {
|
if ($this->application->build_pack === 'dockercompose') {
|
||||||
// Only update if user has permission
|
// Only update if user has permission
|
||||||
|
|
@ -218,9 +294,6 @@ public function mount()
|
||||||
}
|
}
|
||||||
$this->parsedServiceDomains = $sanitizedDomains;
|
$this->parsedServiceDomains = $sanitizedDomains;
|
||||||
|
|
||||||
$this->ports_exposes = $this->application->ports_exposes;
|
|
||||||
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
|
|
||||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
|
||||||
$this->customLabels = $this->application->parseContainerLabels();
|
$this->customLabels = $this->application->parseContainerLabels();
|
||||||
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) {
|
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) {
|
||||||
// Only update custom labels if user has permission
|
// Only update custom labels if user has permission
|
||||||
|
|
@ -249,6 +322,60 @@ public function mount()
|
||||||
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
|
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
|
||||||
$this->dispatch('configurationChanged');
|
$this->dispatch('configurationChanged');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync data from model to properties at the END, after all business logic
|
||||||
|
// This ensures any modifications to $this->application during mount() are reflected in properties
|
||||||
|
$this->syncFromModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getModelBindings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'application.name',
|
||||||
|
'description' => 'application.description',
|
||||||
|
'fqdn' => 'application.fqdn',
|
||||||
|
'git_repository' => 'application.git_repository',
|
||||||
|
'git_branch' => 'application.git_branch',
|
||||||
|
'git_commit_sha' => 'application.git_commit_sha',
|
||||||
|
'install_command' => 'application.install_command',
|
||||||
|
'build_command' => 'application.build_command',
|
||||||
|
'start_command' => 'application.start_command',
|
||||||
|
'build_pack' => 'application.build_pack',
|
||||||
|
'static_image' => 'application.static_image',
|
||||||
|
'base_directory' => 'application.base_directory',
|
||||||
|
'publish_directory' => 'application.publish_directory',
|
||||||
|
'ports_exposes' => 'application.ports_exposes',
|
||||||
|
'ports_mappings' => 'application.ports_mappings',
|
||||||
|
'custom_network_aliases' => 'application.custom_network_aliases',
|
||||||
|
'dockerfile' => 'application.dockerfile',
|
||||||
|
'dockerfile_location' => 'application.dockerfile_location',
|
||||||
|
'dockerfile_target_build' => 'application.dockerfile_target_build',
|
||||||
|
'docker_registry_image_name' => 'application.docker_registry_image_name',
|
||||||
|
'docker_registry_image_tag' => 'application.docker_registry_image_tag',
|
||||||
|
'docker_compose_location' => 'application.docker_compose_location',
|
||||||
|
'docker_compose' => 'application.docker_compose',
|
||||||
|
'docker_compose_raw' => 'application.docker_compose_raw',
|
||||||
|
'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command',
|
||||||
|
'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command',
|
||||||
|
'custom_labels' => 'application.custom_labels',
|
||||||
|
'custom_docker_run_options' => 'application.custom_docker_run_options',
|
||||||
|
'pre_deployment_command' => 'application.pre_deployment_command',
|
||||||
|
'pre_deployment_command_container' => 'application.pre_deployment_command_container',
|
||||||
|
'post_deployment_command' => 'application.post_deployment_command',
|
||||||
|
'post_deployment_command_container' => 'application.post_deployment_command_container',
|
||||||
|
'custom_nginx_configuration' => 'application.custom_nginx_configuration',
|
||||||
|
'is_static' => 'application.settings.is_static',
|
||||||
|
'is_spa' => 'application.settings.is_spa',
|
||||||
|
'is_build_server_enabled' => 'application.settings.is_build_server_enabled',
|
||||||
|
'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled',
|
||||||
|
'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled',
|
||||||
|
'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled',
|
||||||
|
'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled',
|
||||||
|
'http_basic_auth_username' => 'application.http_basic_auth_username',
|
||||||
|
'http_basic_auth_password' => 'application.http_basic_auth_password',
|
||||||
|
'watch_paths' => 'application.watch_paths',
|
||||||
|
'redirect' => 'application.redirect',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
|
|
@ -256,6 +383,12 @@ public function instantSave()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
|
|
||||||
|
$oldPortsExposes = $this->application->ports_exposes;
|
||||||
|
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||||
|
$oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||||
|
|
||||||
|
$this->syncToModel();
|
||||||
|
|
||||||
if ($this->application->settings->isDirty('is_spa')) {
|
if ($this->application->settings->isDirty('is_spa')) {
|
||||||
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
|
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
|
||||||
}
|
}
|
||||||
|
|
@ -265,20 +398,21 @@ public function instantSave()
|
||||||
$this->application->settings->save();
|
$this->application->settings->save();
|
||||||
$this->dispatch('success', 'Settings saved.');
|
$this->dispatch('success', 'Settings saved.');
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
|
|
||||||
// If port_exposes changed, reset default labels
|
// If port_exposes changed, reset default labels
|
||||||
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
|
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||||
$this->resetDefaultLabels(false);
|
$this->resetDefaultLabels(false);
|
||||||
}
|
}
|
||||||
if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
|
if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) {
|
||||||
if ($this->application->settings->is_preserve_repository_enabled === false) {
|
if ($this->is_preserve_repository_enabled === false) {
|
||||||
$this->application->fileStorages->each(function ($storage) {
|
$this->application->fileStorages->each(function ($storage) {
|
||||||
$storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
|
$storage->is_based_on_git = $this->is_preserve_repository_enabled;
|
||||||
$storage->save();
|
$storage->save();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->application->settings->is_container_label_readonly_enabled) {
|
if ($this->is_container_label_readonly_enabled) {
|
||||||
$this->resetDefaultLabels(false);
|
$this->resetDefaultLabels(false);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -366,21 +500,21 @@ public function generateDomain(string $serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedApplicationBaseDirectory()
|
public function updatedBaseDirectory()
|
||||||
{
|
{
|
||||||
if ($this->application->build_pack === 'dockercompose') {
|
if ($this->build_pack === 'dockercompose') {
|
||||||
$this->loadComposeFile();
|
$this->loadComposeFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedApplicationSettingsIsStatic($value)
|
public function updatedIsStatic($value)
|
||||||
{
|
{
|
||||||
if ($value) {
|
if ($value) {
|
||||||
$this->generateNginxConfiguration();
|
$this->generateNginxConfiguration();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedApplicationBuildPack()
|
public function updatedBuildPack()
|
||||||
{
|
{
|
||||||
// Check if user has permission to update
|
// Check if user has permission to update
|
||||||
try {
|
try {
|
||||||
|
|
@ -388,21 +522,28 @@ public function updatedApplicationBuildPack()
|
||||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||||
// User doesn't have permission, revert the change and return
|
// User doesn't have permission, revert the change and return
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->application->build_pack !== 'nixpacks') {
|
// Sync property to model before checking/modifying
|
||||||
|
$this->syncToModel();
|
||||||
|
|
||||||
|
if ($this->build_pack !== 'nixpacks') {
|
||||||
|
$this->is_static = false;
|
||||||
$this->application->settings->is_static = false;
|
$this->application->settings->is_static = false;
|
||||||
$this->application->settings->save();
|
$this->application->settings->save();
|
||||||
} else {
|
} else {
|
||||||
$this->application->ports_exposes = $this->ports_exposes = 3000;
|
$this->ports_exposes = 3000;
|
||||||
|
$this->application->ports_exposes = 3000;
|
||||||
$this->resetDefaultLabels(false);
|
$this->resetDefaultLabels(false);
|
||||||
}
|
}
|
||||||
if ($this->application->build_pack === 'dockercompose') {
|
if ($this->build_pack === 'dockercompose') {
|
||||||
// Only update if user has permission
|
// Only update if user has permission
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
|
$this->fqdn = null;
|
||||||
$this->application->fqdn = null;
|
$this->application->fqdn = null;
|
||||||
$this->application->settings->save();
|
$this->application->settings->save();
|
||||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||||
|
|
@ -421,8 +562,9 @@ public function updatedApplicationBuildPack()
|
||||||
$this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
|
$this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->application->build_pack === 'static') {
|
if ($this->build_pack === 'static') {
|
||||||
$this->application->ports_exposes = $this->ports_exposes = 80;
|
$this->ports_exposes = 80;
|
||||||
|
$this->application->ports_exposes = 80;
|
||||||
$this->resetDefaultLabels(false);
|
$this->resetDefaultLabels(false);
|
||||||
$this->generateNginxConfiguration();
|
$this->generateNginxConfiguration();
|
||||||
}
|
}
|
||||||
|
|
@ -438,8 +580,11 @@ public function getWildcardDomain()
|
||||||
$server = data_get($this->application, 'destination.server');
|
$server = data_get($this->application, 'destination.server');
|
||||||
if ($server) {
|
if ($server) {
|
||||||
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
|
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
|
||||||
$this->application->fqdn = $fqdn;
|
$this->fqdn = $fqdn;
|
||||||
|
$this->syncToModel();
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
$this->resetDefaultLabels();
|
$this->resetDefaultLabels();
|
||||||
$this->dispatch('success', 'Wildcard domain generated.');
|
$this->dispatch('success', 'Wildcard domain generated.');
|
||||||
}
|
}
|
||||||
|
|
@ -453,8 +598,11 @@ public function generateNginxConfiguration($type = 'static')
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
|
|
||||||
$this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
|
$this->custom_nginx_configuration = defaultNginxConfiguration($type);
|
||||||
|
$this->syncToModel();
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
$this->dispatch('success', 'Nginx configuration generated.');
|
$this->dispatch('success', 'Nginx configuration generated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
@ -464,15 +612,16 @@ public function generateNginxConfiguration($type = 'static')
|
||||||
public function resetDefaultLabels($manualReset = false)
|
public function resetDefaultLabels($manualReset = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (! $this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
|
if (! $this->is_container_label_readonly_enabled && ! $manualReset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||||
$this->ports_exposes = $this->application->ports_exposes;
|
$this->custom_labels = base64_encode($this->customLabels);
|
||||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
$this->syncToModel();
|
||||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
if ($this->application->build_pack === 'dockercompose') {
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
|
if ($this->build_pack === 'dockercompose') {
|
||||||
$this->loadComposeFile(showToast: false);
|
$this->loadComposeFile(showToast: false);
|
||||||
}
|
}
|
||||||
$this->dispatch('configurationChanged');
|
$this->dispatch('configurationChanged');
|
||||||
|
|
@ -483,8 +632,8 @@ public function resetDefaultLabels($manualReset = false)
|
||||||
|
|
||||||
public function checkFqdns($showToaster = true)
|
public function checkFqdns($showToaster = true)
|
||||||
{
|
{
|
||||||
if (data_get($this->application, 'fqdn')) {
|
if ($this->fqdn) {
|
||||||
$domains = str($this->application->fqdn)->trim()->explode(',');
|
$domains = str($this->fqdn)->trim()->explode(',');
|
||||||
if ($this->application->additional_servers->count() === 0) {
|
if ($this->application->additional_servers->count() === 0) {
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
if (! validateDNSEntry($domain, $this->application->destination->server)) {
|
if (! validateDNSEntry($domain, $this->application->destination->server)) {
|
||||||
|
|
@ -507,7 +656,8 @@ public function checkFqdns($showToaster = true)
|
||||||
$this->forceSaveDomains = false;
|
$this->forceSaveDomains = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->application->fqdn = $domains->implode(',');
|
$this->fqdn = $domains->implode(',');
|
||||||
|
$this->application->fqdn = $this->fqdn;
|
||||||
$this->resetDefaultLabels(false);
|
$this->resetDefaultLabels(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,21 +697,27 @@ public function submit($showToaster = true)
|
||||||
|
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
$oldPortsExposes = $this->application->ports_exposes;
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
$oldDockerComposeLocation = $this->initialDockerComposeLocation;
|
||||||
|
|
||||||
|
// Process FQDN with intermediate variable to avoid Collection/string confusion
|
||||||
|
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||||
|
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
||||||
|
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||||
$domain = trim($domain);
|
$domain = trim($domain);
|
||||||
Url::fromString($domain, ['http', 'https']);
|
Url::fromString($domain, ['http', 'https']);
|
||||||
|
|
||||||
return str($domain)->lower();
|
return str($domain)->lower();
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
$this->fqdn = $domains->unique()->implode(',');
|
||||||
$warning = sslipDomainWarning($this->application->fqdn);
|
$warning = sslipDomainWarning($this->fqdn);
|
||||||
if ($warning) {
|
if ($warning) {
|
||||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||||
}
|
}
|
||||||
// $this->resetDefaultLabels();
|
|
||||||
|
$this->syncToModel();
|
||||||
|
|
||||||
if ($this->application->isDirty('redirect')) {
|
if ($this->application->isDirty('redirect')) {
|
||||||
$this->setRedirect();
|
$this->setRedirect();
|
||||||
|
|
@ -581,38 +737,42 @@ public function submit($showToaster = true)
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
|
if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) {
|
||||||
$compose_return = $this->loadComposeFile(showToast: false);
|
$compose_return = $this->loadComposeFile(showToast: false);
|
||||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
|
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||||
$this->resetDefaultLabels();
|
$this->resetDefaultLabels();
|
||||||
}
|
}
|
||||||
if (data_get($this->application, 'build_pack') === 'dockerimage') {
|
if ($this->build_pack === 'dockerimage') {
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'application.docker_registry_image_name' => 'required',
|
'docker_registry_image_name' => 'required',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data_get($this->application, 'custom_docker_run_options')) {
|
if ($this->custom_docker_run_options) {
|
||||||
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
|
$this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString();
|
||||||
|
$this->application->custom_docker_run_options = $this->custom_docker_run_options;
|
||||||
}
|
}
|
||||||
if (data_get($this->application, 'dockerfile')) {
|
if ($this->dockerfile) {
|
||||||
$port = get_port_from_dockerfile($this->application->dockerfile);
|
$port = get_port_from_dockerfile($this->dockerfile);
|
||||||
if ($port && ! $this->application->ports_exposes) {
|
if ($port && ! $this->ports_exposes) {
|
||||||
|
$this->ports_exposes = $port;
|
||||||
$this->application->ports_exposes = $port;
|
$this->application->ports_exposes = $port;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->application->base_directory && $this->application->base_directory !== '/') {
|
if ($this->base_directory && $this->base_directory !== '/') {
|
||||||
$this->application->base_directory = rtrim($this->application->base_directory, '/');
|
$this->base_directory = rtrim($this->base_directory, '/');
|
||||||
|
$this->application->base_directory = $this->base_directory;
|
||||||
}
|
}
|
||||||
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
|
if ($this->publish_directory && $this->publish_directory !== '/') {
|
||||||
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
|
$this->publish_directory = rtrim($this->publish_directory, '/');
|
||||||
|
$this->application->publish_directory = $this->publish_directory;
|
||||||
}
|
}
|
||||||
if ($this->application->build_pack === 'dockercompose') {
|
if ($this->build_pack === 'dockercompose') {
|
||||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||||
if ($this->application->isDirty('docker_compose_domains')) {
|
if ($this->application->isDirty('docker_compose_domains')) {
|
||||||
foreach ($this->parsedServiceDomains as $service) {
|
foreach ($this->parsedServiceDomains as $service) {
|
||||||
|
|
@ -643,12 +803,12 @@ public function submit($showToaster = true)
|
||||||
}
|
}
|
||||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
$this->application->refresh();
|
||||||
if ($originalFqdn !== $this->application->fqdn) {
|
$this->syncFromModel();
|
||||||
$this->application->fqdn = $originalFqdn;
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,34 @@ class Previews extends Component
|
||||||
|
|
||||||
public $pendingPreviewId = null;
|
public $pendingPreviewId = null;
|
||||||
|
|
||||||
|
public array $previewFqdns = [];
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'application.previews.*.fqdn' => 'string|nullable',
|
'previewFqdns.*' => 'string|nullable',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->pull_requests = collect();
|
$this->pull_requests = collect();
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
|
$this->syncData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
foreach ($this->previewFqdns as $key => $fqdn) {
|
||||||
|
$preview = $this->application->previews->get($key);
|
||||||
|
if ($preview) {
|
||||||
|
$preview->fqdn = $fqdn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->previewFqdns = [];
|
||||||
|
foreach ($this->application->previews as $key => $preview) {
|
||||||
|
$this->previewFqdns[$key] = $preview->fqdn;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load_prs()
|
public function load_prs()
|
||||||
|
|
@ -73,35 +93,52 @@ public function save_preview($preview_id)
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
$success = true;
|
$success = true;
|
||||||
$preview = $this->application->previews->find($preview_id);
|
$preview = $this->application->previews->find($preview_id);
|
||||||
if (data_get_str($preview, 'fqdn')->isNotEmpty()) {
|
|
||||||
$preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim();
|
|
||||||
$preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim();
|
|
||||||
$preview->fqdn = str($preview->fqdn)->trim()->lower();
|
|
||||||
if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) {
|
|
||||||
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
|
||||||
$success = false;
|
|
||||||
}
|
|
||||||
// Check for domain conflicts if not forcing save
|
|
||||||
if (! $this->forceSaveDomains) {
|
|
||||||
$result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn);
|
|
||||||
if ($result['hasConflicts']) {
|
|
||||||
$this->domainConflicts = $result['conflicts'];
|
|
||||||
$this->showDomainConflictModal = true;
|
|
||||||
$this->pendingPreviewId = $preview_id;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reset the force flag after using it
|
|
||||||
$this->forceSaveDomains = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $preview) {
|
if (! $preview) {
|
||||||
throw new \Exception('Preview not found');
|
throw new \Exception('Preview not found');
|
||||||
}
|
}
|
||||||
$success && $preview->save();
|
|
||||||
$success && $this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
|
// Find the key for this preview in the collection
|
||||||
|
$previewKey = $this->application->previews->search(function ($item) use ($preview_id) {
|
||||||
|
return $item->id == $preview_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) {
|
||||||
|
$fqdn = $this->previewFqdns[$previewKey];
|
||||||
|
|
||||||
|
if (! empty($fqdn)) {
|
||||||
|
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
|
||||||
|
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||||
|
$fqdn = str($fqdn)->trim()->lower();
|
||||||
|
$this->previewFqdns[$previewKey] = $fqdn;
|
||||||
|
|
||||||
|
if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
|
||||||
|
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||||
|
$success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for domain conflicts if not forcing save
|
||||||
|
if (! $this->forceSaveDomains) {
|
||||||
|
$result = checkDomainUsage(resource: $this->application, domain: $fqdn);
|
||||||
|
if ($result['hasConflicts']) {
|
||||||
|
$this->domainConflicts = $result['conflicts'];
|
||||||
|
$this->showDomainConflictModal = true;
|
||||||
|
$this->pendingPreviewId = $preview_id;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset the force flag after using it
|
||||||
|
$this->forceSaveDomains = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$this->syncData(true);
|
||||||
|
$preview->save();
|
||||||
|
$this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +158,7 @@ public function generate_preview($preview_id)
|
||||||
if ($this->application->build_pack === 'dockercompose') {
|
if ($this->application->build_pack === 'dockercompose') {
|
||||||
$preview->generate_preview_fqdn_compose();
|
$preview->generate_preview_fqdn_compose();
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
$this->dispatch('success', 'Domain generated.');
|
$this->dispatch('success', 'Domain generated.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -128,6 +166,7 @@ public function generate_preview($preview_id)
|
||||||
|
|
||||||
$preview->generate_preview_fqdn();
|
$preview->generate_preview_fqdn();
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
$this->dispatch('update_links');
|
$this->dispatch('update_links');
|
||||||
$this->dispatch('success', 'Domain generated.');
|
$this->dispatch('success', 'Domain generated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -152,6 +191,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
|
||||||
}
|
}
|
||||||
$found->generate_preview_fqdn_compose();
|
$found->generate_preview_fqdn_compose();
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
} else {
|
} else {
|
||||||
$this->setDeploymentUuid();
|
$this->setDeploymentUuid();
|
||||||
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||||
|
|
@ -164,6 +204,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
|
||||||
}
|
}
|
||||||
$found->generate_preview_fqdn();
|
$found->generate_preview_fqdn();
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
$this->dispatch('update_links');
|
$this->dispatch('update_links');
|
||||||
$this->dispatch('success', 'Preview added.');
|
$this->dispatch('success', 'Preview added.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ class PreviewsCompose extends Component
|
||||||
|
|
||||||
public ApplicationPreview $preview;
|
public ApplicationPreview $preview;
|
||||||
|
|
||||||
|
public ?string $domain = null;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->domain = data_get($this->service, 'domain');
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.project.application.previews-compose');
|
return view('livewire.project.application.previews-compose');
|
||||||
|
|
@ -28,10 +35,10 @@ public function save()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->preview->application);
|
$this->authorize('update', $this->preview->application);
|
||||||
|
|
||||||
$domain = data_get($this->service, 'domain');
|
|
||||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
|
||||||
$docker_compose_domains[$this->serviceName]['domain'] = $domain;
|
$docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? [];
|
||||||
|
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
|
||||||
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
||||||
$this->preview->save();
|
$this->preview->save();
|
||||||
$this->dispatch('update_links');
|
$this->dispatch('update_links');
|
||||||
|
|
@ -46,7 +53,7 @@ public function generate()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->preview->application);
|
$this->authorize('update', $this->preview->application);
|
||||||
|
|
||||||
$domains = collect(json_decode($this->preview->application->docker_compose_domains)) ?? collect();
|
$domains = collect(json_decode($this->preview->application->docker_compose_domains, true) ?: []);
|
||||||
$domain = $domains->first(function ($_, $key) {
|
$domain = $domains->first(function ($_, $key) {
|
||||||
return $key === $this->serviceName;
|
return $key === $this->serviceName;
|
||||||
});
|
});
|
||||||
|
|
@ -68,24 +75,40 @@ public function generate()
|
||||||
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
|
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
|
||||||
} else {
|
} else {
|
||||||
// Use the existing domain from the main application
|
// Use the existing domain from the main application
|
||||||
$url = Url::fromString($domain_string);
|
// Handle multiple domains separated by commas
|
||||||
|
$domain_list = explode(',', $domain_string);
|
||||||
|
$preview_fqdns = [];
|
||||||
$template = $this->preview->application->preview_url_template;
|
$template = $this->preview->application->preview_url_template;
|
||||||
$host = $url->getHost();
|
|
||||||
$schema = $url->getScheme();
|
|
||||||
$portInt = $url->getPort();
|
|
||||||
$port = $portInt !== null ? ':'.$portInt : '';
|
|
||||||
$random = new Cuid2;
|
$random = new Cuid2;
|
||||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
|
||||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
foreach ($domain_list as $single_domain) {
|
||||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
$single_domain = trim($single_domain);
|
||||||
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
|
if (empty($single_domain)) {
|
||||||
$preview_fqdn = "$schema://$preview_fqdn";
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = Url::fromString($single_domain);
|
||||||
|
$host = $url->getHost();
|
||||||
|
$schema = $url->getScheme();
|
||||||
|
$portInt = $url->getPort();
|
||||||
|
$port = $portInt !== null ? ':'.$portInt : '';
|
||||||
|
|
||||||
|
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||||
|
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||||
|
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||||
|
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
|
||||||
|
$preview_fqdns[] = "$schema://$preview_fqdn";
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview_fqdn = implode(',', $preview_fqdns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the generated domain
|
// Save the generated domain
|
||||||
|
$this->domain = $preview_fqdn;
|
||||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
|
||||||
$docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn;
|
$docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? [];
|
||||||
|
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
|
||||||
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
||||||
$this->preview->save();
|
$this->preview->save();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
class Configuration extends Component
|
class Configuration extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests;
|
use AuthorizesRequests;
|
||||||
|
|
||||||
public $currentRoute;
|
public $currentRoute;
|
||||||
|
|
||||||
public $database;
|
public $database;
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,30 @@ class Database extends Component
|
||||||
|
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
||||||
|
public ?string $humanName = null;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
|
public ?string $image = null;
|
||||||
|
|
||||||
|
public bool $excludeFromStatus = false;
|
||||||
|
|
||||||
|
public ?int $publicPort = null;
|
||||||
|
|
||||||
|
public bool $isPublic = false;
|
||||||
|
|
||||||
|
public bool $isLogDrainEnabled = false;
|
||||||
|
|
||||||
protected $listeners = ['refreshFileStorages'];
|
protected $listeners = ['refreshFileStorages'];
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'database.human_name' => 'nullable',
|
'humanName' => 'nullable',
|
||||||
'database.description' => 'nullable',
|
'description' => 'nullable',
|
||||||
'database.image' => 'required',
|
'image' => 'required',
|
||||||
'database.exclude_from_status' => 'required|boolean',
|
'excludeFromStatus' => 'required|boolean',
|
||||||
'database.public_port' => 'nullable|integer',
|
'publicPort' => 'nullable|integer',
|
||||||
'database.is_public' => 'required|boolean',
|
'isPublic' => 'required|boolean',
|
||||||
'database.is_log_drain_enabled' => 'required|boolean',
|
'isLogDrainEnabled' => 'required|boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
@ -50,11 +64,33 @@ public function mount()
|
||||||
$this->db_url_public = $this->database->getServiceDatabaseUrl();
|
$this->db_url_public = $this->database->getServiceDatabaseUrl();
|
||||||
}
|
}
|
||||||
$this->refreshFileStorages();
|
$this->refreshFileStorages();
|
||||||
|
$this->syncData(false);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
$this->database->human_name = $this->humanName;
|
||||||
|
$this->database->description = $this->description;
|
||||||
|
$this->database->image = $this->image;
|
||||||
|
$this->database->exclude_from_status = $this->excludeFromStatus;
|
||||||
|
$this->database->public_port = $this->publicPort;
|
||||||
|
$this->database->is_public = $this->isPublic;
|
||||||
|
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||||
|
} else {
|
||||||
|
$this->humanName = $this->database->human_name;
|
||||||
|
$this->description = $this->database->description;
|
||||||
|
$this->image = $this->database->image;
|
||||||
|
$this->excludeFromStatus = $this->database->exclude_from_status ?? false;
|
||||||
|
$this->publicPort = $this->database->public_port;
|
||||||
|
$this->isPublic = $this->database->is_public ?? false;
|
||||||
|
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function delete($password)
|
public function delete($password)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -92,7 +128,7 @@ public function instantSaveLogDrain()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->database);
|
$this->authorize('update', $this->database);
|
||||||
if (! $this->database->service->destination->server->isLogDrainEnabled()) {
|
if (! $this->database->service->destination->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->isLogDrainEnabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -145,15 +181,17 @@ public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->database);
|
$this->authorize('update', $this->database);
|
||||||
if ($this->database->is_public && ! $this->database->public_port) {
|
if ($this->isPublic && ! $this->publicPort) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->isPublic = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$this->syncData(true);
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (! str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
|
$this->isPublic = false;
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -182,7 +220,10 @@ public function submit()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->database);
|
$this->authorize('update', $this->database);
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
$this->syncData(true);
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
|
$this->database->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
updateCompose($this->database);
|
updateCompose($this->database);
|
||||||
$this->dispatch('success', 'Database saved.');
|
$this->dispatch('success', 'Database saved.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ class EditCompose extends Component
|
||||||
|
|
||||||
public $serviceId;
|
public $serviceId;
|
||||||
|
|
||||||
|
public ?string $dockerComposeRaw = null;
|
||||||
|
|
||||||
|
public ?string $dockerCompose = null;
|
||||||
|
|
||||||
|
public bool $isContainerLabelEscapeEnabled = false;
|
||||||
|
|
||||||
protected $listeners = [
|
protected $listeners = [
|
||||||
'refreshEnvs',
|
'refreshEnvs',
|
||||||
'envsUpdated',
|
'envsUpdated',
|
||||||
|
|
@ -18,30 +24,45 @@ class EditCompose extends Component
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'service.docker_compose_raw' => 'required',
|
'dockerComposeRaw' => 'required',
|
||||||
'service.docker_compose' => 'required',
|
'dockerCompose' => 'required',
|
||||||
'service.is_container_label_escape_enabled' => 'required',
|
'isContainerLabelEscapeEnabled' => 'required',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function envsUpdated()
|
public function envsUpdated()
|
||||||
{
|
{
|
||||||
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
|
$this->dispatch('saveCompose', $this->dockerComposeRaw);
|
||||||
$this->refreshEnvs();
|
$this->refreshEnvs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refreshEnvs()
|
public function refreshEnvs()
|
||||||
{
|
{
|
||||||
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
||||||
|
$this->syncData(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
|
||||||
|
$this->syncData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
$this->service->docker_compose_raw = $this->dockerComposeRaw;
|
||||||
|
$this->service->docker_compose = $this->dockerCompose;
|
||||||
|
$this->service->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled;
|
||||||
|
} else {
|
||||||
|
$this->dockerComposeRaw = $this->service->docker_compose_raw;
|
||||||
|
$this->dockerCompose = $this->service->docker_compose;
|
||||||
|
$this->isContainerLabelEscapeEnabled = $this->service->is_container_label_escape_enabled ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validateCompose()
|
public function validateCompose()
|
||||||
{
|
{
|
||||||
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id);
|
$isValid = validateComposeFile($this->dockerComposeRaw, $this->service->server_id);
|
||||||
if ($isValid !== 'OK') {
|
if ($isValid !== 'OK') {
|
||||||
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -52,16 +73,17 @@ public function validateCompose()
|
||||||
public function saveEditedCompose()
|
public function saveEditedCompose()
|
||||||
{
|
{
|
||||||
$this->dispatch('info', 'Saving new docker compose...');
|
$this->dispatch('info', 'Saving new docker compose...');
|
||||||
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
|
$this->dispatch('saveCompose', $this->dockerComposeRaw);
|
||||||
$this->dispatch('refreshStorages');
|
$this->dispatch('refreshStorages');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'service.is_container_label_escape_enabled' => 'required',
|
'isContainerLabelEscapeEnabled' => 'required',
|
||||||
]);
|
]);
|
||||||
$this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]);
|
$this->syncData(true);
|
||||||
|
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
|
||||||
$this->dispatch('success', 'Service updated successfully');
|
$this->dispatch('success', 'Service updated successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
namespace App\Livewire\Project\Service;
|
namespace App\Livewire\Project\Service;
|
||||||
|
|
||||||
|
use App\Livewire\Concerns\SynchronizesModelData;
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
|
|
||||||
class EditDomain extends Component
|
class EditDomain extends Component
|
||||||
{
|
{
|
||||||
|
use SynchronizesModelData;
|
||||||
public $applicationId;
|
public $applicationId;
|
||||||
|
|
||||||
public ServiceApplication $application;
|
public ServiceApplication $application;
|
||||||
|
|
@ -18,14 +20,24 @@ class EditDomain extends Component
|
||||||
|
|
||||||
public $forceSaveDomains = false;
|
public $forceSaveDomains = false;
|
||||||
|
|
||||||
|
public ?string $fqdn = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'application.fqdn' => 'nullable',
|
'fqdn' => 'nullable',
|
||||||
'application.required_fqdn' => 'required|boolean',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->application = ServiceApplication::find($this->applicationId);
|
$this->application = ServiceApplication::query()->findOrFail($this->applicationId);
|
||||||
|
$this->authorize('view', $this->application);
|
||||||
|
$this->syncFromModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getModelBindings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'fqdn' => 'application.fqdn',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function confirmDomainUsage()
|
public function confirmDomainUsage()
|
||||||
|
|
@ -38,19 +50,22 @@ public function confirmDomainUsage()
|
||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
$this->authorize('update', $this->application);
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
||||||
|
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||||
$domain = trim($domain);
|
$domain = trim($domain);
|
||||||
Url::fromString($domain, ['http', 'https']);
|
Url::fromString($domain, ['http', 'https']);
|
||||||
|
|
||||||
return str($domain)->lower();
|
return str($domain)->lower();
|
||||||
});
|
});
|
||||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
$this->fqdn = $domains->unique()->implode(',');
|
||||||
$warning = sslipDomainWarning($this->application->fqdn);
|
$warning = sslipDomainWarning($this->fqdn);
|
||||||
if ($warning) {
|
if ($warning) {
|
||||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||||
}
|
}
|
||||||
|
// Sync to model for domain conflict check
|
||||||
|
$this->syncToModel();
|
||||||
// Check for domain conflicts if not forcing save
|
// Check for domain conflicts if not forcing save
|
||||||
if (! $this->forceSaveDomains) {
|
if (! $this->forceSaveDomains) {
|
||||||
$result = checkDomainUsage(resource: $this->application);
|
$result = checkDomainUsage(resource: $this->application);
|
||||||
|
|
@ -67,6 +82,8 @@ public function submit()
|
||||||
|
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
|
$this->application->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
updateCompose($this->application);
|
updateCompose($this->application);
|
||||||
if (str($this->application->fqdn)->contains(',')) {
|
if (str($this->application->fqdn)->contains(',')) {
|
||||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||||
|
|
@ -79,6 +96,7 @@ public function submit()
|
||||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||||
if ($originalFqdn !== $this->application->fqdn) {
|
if ($originalFqdn !== $this->application->fqdn) {
|
||||||
$this->application->fqdn = $originalFqdn;
|
$this->application->fqdn = $originalFqdn;
|
||||||
|
$this->syncData(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire\Project\Service;
|
namespace App\Livewire\Project\Service;
|
||||||
|
|
||||||
|
use App\Livewire\Concerns\SynchronizesModelData;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\LocalFileVolume;
|
use App\Models\LocalFileVolume;
|
||||||
|
|
@ -22,7 +23,7 @@
|
||||||
|
|
||||||
class FileStorage extends Component
|
class FileStorage extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests;
|
use AuthorizesRequests, SynchronizesModelData;
|
||||||
|
|
||||||
public LocalFileVolume $fileStorage;
|
public LocalFileVolume $fileStorage;
|
||||||
|
|
||||||
|
|
@ -36,12 +37,16 @@ class FileStorage extends Component
|
||||||
|
|
||||||
public bool $isReadOnly = false;
|
public bool $isReadOnly = false;
|
||||||
|
|
||||||
|
public ?string $content = null;
|
||||||
|
|
||||||
|
public bool $isBasedOnGit = false;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'fileStorage.is_directory' => 'required',
|
'fileStorage.is_directory' => 'required',
|
||||||
'fileStorage.fs_path' => 'required',
|
'fileStorage.fs_path' => 'required',
|
||||||
'fileStorage.mount_path' => 'required',
|
'fileStorage.mount_path' => 'required',
|
||||||
'fileStorage.content' => 'nullable',
|
'content' => 'nullable',
|
||||||
'fileStorage.is_based_on_git' => 'required|boolean',
|
'isBasedOnGit' => 'required|boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
|
|
@ -56,6 +61,15 @@ public function mount()
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||||
|
$this->syncFromModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getModelBindings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'content' => 'fileStorage.content',
|
||||||
|
'isBasedOnGit' => 'fileStorage.is_based_on_git',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function convertToDirectory()
|
public function convertToDirectory()
|
||||||
|
|
@ -82,6 +96,7 @@ public function loadStorageOnServer()
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
|
|
||||||
$this->fileStorage->loadStorageOnServer();
|
$this->fileStorage->loadStorageOnServer();
|
||||||
|
$this->syncFromModel();
|
||||||
$this->dispatch('success', 'File storage loaded from server.');
|
$this->dispatch('success', 'File storage loaded from server.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
@ -148,14 +163,16 @@ public function submit()
|
||||||
try {
|
try {
|
||||||
$this->validate();
|
$this->validate();
|
||||||
if ($this->fileStorage->is_directory) {
|
if ($this->fileStorage->is_directory) {
|
||||||
$this->fileStorage->content = null;
|
$this->content = null;
|
||||||
}
|
}
|
||||||
|
$this->syncToModel();
|
||||||
$this->fileStorage->save();
|
$this->fileStorage->save();
|
||||||
$this->fileStorage->saveStorageOnServer();
|
$this->fileStorage->saveStorageOnServer();
|
||||||
$this->dispatch('success', 'File updated.');
|
$this->dispatch('success', 'File updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->fileStorage->setRawAttributes($original);
|
$this->fileStorage->setRawAttributes($original);
|
||||||
$this->fileStorage->save();
|
$this->fileStorage->save();
|
||||||
|
$this->syncFromModel();
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire\Project\Service;
|
namespace App\Livewire\Project\Service;
|
||||||
|
|
||||||
|
use App\Livewire\Concerns\SynchronizesModelData;
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
class ServiceApplicationView extends Component
|
class ServiceApplicationView extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests;
|
use AuthorizesRequests;
|
||||||
|
use SynchronizesModelData;
|
||||||
|
|
||||||
public ServiceApplication $application;
|
public ServiceApplication $application;
|
||||||
|
|
||||||
|
|
@ -29,16 +31,32 @@ class ServiceApplicationView extends Component
|
||||||
|
|
||||||
public $forceSaveDomains = false;
|
public $forceSaveDomains = false;
|
||||||
|
|
||||||
|
public ?string $humanName = null;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
|
public ?string $fqdn = null;
|
||||||
|
|
||||||
|
public ?string $image = null;
|
||||||
|
|
||||||
|
public bool $excludeFromStatus = false;
|
||||||
|
|
||||||
|
public bool $isLogDrainEnabled = false;
|
||||||
|
|
||||||
|
public bool $isGzipEnabled = false;
|
||||||
|
|
||||||
|
public bool $isStripprefixEnabled = false;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'application.human_name' => 'nullable',
|
'humanName' => 'nullable',
|
||||||
'application.description' => 'nullable',
|
'description' => 'nullable',
|
||||||
'application.fqdn' => 'nullable',
|
'fqdn' => 'nullable',
|
||||||
'application.image' => 'string|nullable',
|
'image' => 'string|nullable',
|
||||||
'application.exclude_from_status' => 'required|boolean',
|
'excludeFromStatus' => 'required|boolean',
|
||||||
'application.required_fqdn' => 'required|boolean',
|
'application.required_fqdn' => 'required|boolean',
|
||||||
'application.is_log_drain_enabled' => 'nullable|boolean',
|
'isLogDrainEnabled' => 'nullable|boolean',
|
||||||
'application.is_gzip_enabled' => 'nullable|boolean',
|
'isGzipEnabled' => 'nullable|boolean',
|
||||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
'isStripprefixEnabled' => 'nullable|boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
|
|
@ -56,11 +74,12 @@ public function instantSaveAdvanced()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
if (! $this->application->service->destination->server->isLogDrainEnabled()) {
|
if (! $this->application->service->destination->server->isLogDrainEnabled()) {
|
||||||
$this->application->is_log_drain_enabled = false;
|
$this->isLogDrainEnabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$this->syncToModel();
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -95,11 +114,26 @@ public function mount()
|
||||||
try {
|
try {
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
$this->authorize('view', $this->application);
|
$this->authorize('view', $this->application);
|
||||||
|
$this->syncFromModel();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getModelBindings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'humanName' => 'application.human_name',
|
||||||
|
'description' => 'application.description',
|
||||||
|
'fqdn' => 'application.fqdn',
|
||||||
|
'image' => 'application.image',
|
||||||
|
'excludeFromStatus' => 'application.exclude_from_status',
|
||||||
|
'isLogDrainEnabled' => 'application.is_log_drain_enabled',
|
||||||
|
'isGzipEnabled' => 'application.is_gzip_enabled',
|
||||||
|
'isStripprefixEnabled' => 'application.is_stripprefix_enabled',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function convertToDatabase()
|
public function convertToDatabase()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -146,19 +180,21 @@ public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->application);
|
$this->authorize('update', $this->application);
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
|
||||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||||
$domain = trim($domain);
|
$domain = trim($domain);
|
||||||
Url::fromString($domain, ['http', 'https']);
|
Url::fromString($domain, ['http', 'https']);
|
||||||
|
|
||||||
return str($domain)->lower();
|
return str($domain)->lower();
|
||||||
});
|
});
|
||||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
$this->fqdn = $domains->unique()->implode(',');
|
||||||
$warning = sslipDomainWarning($this->application->fqdn);
|
$warning = sslipDomainWarning($this->fqdn);
|
||||||
if ($warning) {
|
if ($warning) {
|
||||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||||
}
|
}
|
||||||
|
// Sync to model for domain conflict check
|
||||||
|
$this->syncToModel();
|
||||||
// Check for domain conflicts if not forcing save
|
// Check for domain conflicts if not forcing save
|
||||||
if (! $this->forceSaveDomains) {
|
if (! $this->forceSaveDomains) {
|
||||||
$result = checkDomainUsage(resource: $this->application);
|
$result = checkDomainUsage(resource: $this->application);
|
||||||
|
|
@ -175,6 +211,8 @@ public function submit()
|
||||||
|
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
|
$this->application->refresh();
|
||||||
|
$this->syncFromModel();
|
||||||
updateCompose($this->application);
|
updateCompose($this->application);
|
||||||
if (str($this->application->fqdn)->contains(',')) {
|
if (str($this->application->fqdn)->contains(',')) {
|
||||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||||
|
|
@ -186,6 +224,7 @@ public function submit()
|
||||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||||
if ($originalFqdn !== $this->application->fqdn) {
|
if ($originalFqdn !== $this->application->fqdn) {
|
||||||
$this->application->fqdn = $originalFqdn;
|
$this->application->fqdn = $originalFqdn;
|
||||||
|
$this->syncFromModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,25 @@ class StackForm extends Component
|
||||||
|
|
||||||
protected $listeners = ['saveCompose'];
|
protected $listeners = ['saveCompose'];
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
|
public string $dockerComposeRaw;
|
||||||
|
|
||||||
|
public string $dockerCompose;
|
||||||
|
|
||||||
|
public ?bool $connectToDockerNetwork = null;
|
||||||
|
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
$baseRules = [
|
$baseRules = [
|
||||||
'service.docker_compose_raw' => 'required',
|
'dockerComposeRaw' => 'required',
|
||||||
'service.docker_compose' => 'required',
|
'dockerCompose' => 'required',
|
||||||
'service.name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'service.description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'service.connect_to_docker_network' => 'nullable',
|
'connectToDockerNetwork' => 'nullable',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add dynamic field rules
|
// Add dynamic field rules
|
||||||
|
|
@ -39,19 +50,44 @@ protected function messages(): array
|
||||||
return array_merge(
|
return array_merge(
|
||||||
ValidationPatterns::combinedMessages(),
|
ValidationPatterns::combinedMessages(),
|
||||||
[
|
[
|
||||||
'service.name.required' => 'The Name field is required.',
|
'name.required' => 'The Name field is required.',
|
||||||
'service.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||||
'service.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||||
'service.docker_compose_raw.required' => 'The Docker Compose Raw field is required.',
|
'dockerComposeRaw.required' => 'The Docker Compose Raw field is required.',
|
||||||
'service.docker_compose.required' => 'The Docker Compose field is required.',
|
'dockerCompose.required' => 'The Docker Compose field is required.',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $validationAttributes = [];
|
public $validationAttributes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// Sync TO model (before save)
|
||||||
|
$this->service->name = $this->name;
|
||||||
|
$this->service->description = $this->description;
|
||||||
|
$this->service->docker_compose_raw = $this->dockerComposeRaw;
|
||||||
|
$this->service->docker_compose = $this->dockerCompose;
|
||||||
|
$this->service->connect_to_docker_network = $this->connectToDockerNetwork;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->name = $this->service->name;
|
||||||
|
$this->description = $this->service->description;
|
||||||
|
$this->dockerComposeRaw = $this->service->docker_compose_raw;
|
||||||
|
$this->dockerCompose = $this->service->docker_compose;
|
||||||
|
$this->connectToDockerNetwork = $this->service->connect_to_docker_network;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
$this->syncData(false);
|
||||||
$this->fields = collect([]);
|
$this->fields = collect([]);
|
||||||
$extraFields = $this->service->extraFields();
|
$extraFields = $this->service->extraFields();
|
||||||
foreach ($extraFields as $serviceName => $fields) {
|
foreach ($extraFields as $serviceName => $fields) {
|
||||||
|
|
@ -87,12 +123,13 @@ public function mount()
|
||||||
|
|
||||||
public function saveCompose($raw)
|
public function saveCompose($raw)
|
||||||
{
|
{
|
||||||
$this->service->docker_compose_raw = $raw;
|
$this->dockerComposeRaw = $raw;
|
||||||
$this->submit(notify: true);
|
$this->submit(notify: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
|
$this->syncData(true);
|
||||||
$this->service->save();
|
$this->service->save();
|
||||||
$this->dispatch('success', 'Service settings saved.');
|
$this->dispatch('success', 'Service settings saved.');
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +138,7 @@ public function submit($notify = true)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
$this->syncData(true);
|
||||||
|
|
||||||
// Validate for command injection BEFORE saving to database
|
// Validate for command injection BEFORE saving to database
|
||||||
validateDockerComposeForInjection($this->service->docker_compose_raw);
|
validateDockerComposeForInjection($this->service->docker_compose_raw);
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,90 @@
|
||||||
|
|
||||||
namespace App\Livewire\Project\Shared;
|
namespace App\Livewire\Project\Shared;
|
||||||
|
|
||||||
|
use App\Livewire\Concerns\SynchronizesModelData;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class HealthChecks extends Component
|
class HealthChecks extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests;
|
use AuthorizesRequests;
|
||||||
|
use SynchronizesModelData;
|
||||||
|
|
||||||
public $resource;
|
public $resource;
|
||||||
|
|
||||||
protected $rules = [
|
// Explicit properties
|
||||||
'resource.health_check_enabled' => 'boolean',
|
public bool $healthCheckEnabled = false;
|
||||||
'resource.health_check_path' => 'string',
|
|
||||||
'resource.health_check_port' => 'nullable|string',
|
|
||||||
'resource.health_check_host' => 'string',
|
|
||||||
'resource.health_check_method' => 'string',
|
|
||||||
'resource.health_check_return_code' => 'integer',
|
|
||||||
'resource.health_check_scheme' => 'string',
|
|
||||||
'resource.health_check_response_text' => 'nullable|string',
|
|
||||||
'resource.health_check_interval' => 'integer|min:1',
|
|
||||||
'resource.health_check_timeout' => 'integer|min:1',
|
|
||||||
'resource.health_check_retries' => 'integer|min:1',
|
|
||||||
'resource.health_check_start_period' => 'integer',
|
|
||||||
'resource.custom_healthcheck_found' => 'boolean',
|
|
||||||
|
|
||||||
|
public string $healthCheckMethod;
|
||||||
|
|
||||||
|
public string $healthCheckScheme;
|
||||||
|
|
||||||
|
public string $healthCheckHost;
|
||||||
|
|
||||||
|
public ?string $healthCheckPort = null;
|
||||||
|
|
||||||
|
public string $healthCheckPath;
|
||||||
|
|
||||||
|
public int $healthCheckReturnCode;
|
||||||
|
|
||||||
|
public ?string $healthCheckResponseText = null;
|
||||||
|
|
||||||
|
public int $healthCheckInterval;
|
||||||
|
|
||||||
|
public int $healthCheckTimeout;
|
||||||
|
|
||||||
|
public int $healthCheckRetries;
|
||||||
|
|
||||||
|
public int $healthCheckStartPeriod;
|
||||||
|
|
||||||
|
public bool $customHealthcheckFound = false;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'healthCheckEnabled' => 'boolean',
|
||||||
|
'healthCheckPath' => 'string',
|
||||||
|
'healthCheckPort' => 'nullable|string',
|
||||||
|
'healthCheckHost' => 'string',
|
||||||
|
'healthCheckMethod' => 'string',
|
||||||
|
'healthCheckReturnCode' => 'integer',
|
||||||
|
'healthCheckScheme' => 'string',
|
||||||
|
'healthCheckResponseText' => 'nullable|string',
|
||||||
|
'healthCheckInterval' => 'integer|min:1',
|
||||||
|
'healthCheckTimeout' => 'integer|min:1',
|
||||||
|
'healthCheckRetries' => 'integer|min:1',
|
||||||
|
'healthCheckStartPeriod' => 'integer',
|
||||||
|
'customHealthcheckFound' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected function getModelBindings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'healthCheckEnabled' => 'resource.health_check_enabled',
|
||||||
|
'healthCheckMethod' => 'resource.health_check_method',
|
||||||
|
'healthCheckScheme' => 'resource.health_check_scheme',
|
||||||
|
'healthCheckHost' => 'resource.health_check_host',
|
||||||
|
'healthCheckPort' => 'resource.health_check_port',
|
||||||
|
'healthCheckPath' => 'resource.health_check_path',
|
||||||
|
'healthCheckReturnCode' => 'resource.health_check_return_code',
|
||||||
|
'healthCheckResponseText' => 'resource.health_check_response_text',
|
||||||
|
'healthCheckInterval' => 'resource.health_check_interval',
|
||||||
|
'healthCheckTimeout' => 'resource.health_check_timeout',
|
||||||
|
'healthCheckRetries' => 'resource.health_check_retries',
|
||||||
|
'healthCheckStartPeriod' => 'resource.health_check_start_period',
|
||||||
|
'customHealthcheckFound' => 'resource.custom_healthcheck_found',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->authorize('view', $this->resource);
|
||||||
|
$this->syncFromModel();
|
||||||
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
|
|
||||||
|
$this->syncToModel();
|
||||||
$this->resource->save();
|
$this->resource->save();
|
||||||
$this->dispatch('success', 'Health check updated.');
|
$this->dispatch('success', 'Health check updated.');
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +95,8 @@ public function submit()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
|
$this->syncToModel();
|
||||||
$this->resource->save();
|
$this->resource->save();
|
||||||
$this->dispatch('success', 'Health check updated.');
|
$this->dispatch('success', 'Health check updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -51,14 +108,16 @@ public function toggleHealthcheck()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
$wasEnabled = $this->resource->health_check_enabled;
|
$wasEnabled = $this->healthCheckEnabled;
|
||||||
$this->resource->health_check_enabled = ! $this->resource->health_check_enabled;
|
$this->healthCheckEnabled = ! $this->healthCheckEnabled;
|
||||||
|
|
||||||
|
$this->syncToModel();
|
||||||
$this->resource->save();
|
$this->resource->save();
|
||||||
|
|
||||||
if ($this->resource->health_check_enabled && ! $wasEnabled && $this->resource->isRunning()) {
|
if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) {
|
||||||
$this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.');
|
$this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.');
|
||||||
} else {
|
} else {
|
||||||
$this->dispatch('success', 'Health check '.($this->resource->health_check_enabled ? 'enabled' : 'disabled').'.');
|
$this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'.');
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
|
||||||
|
|
@ -11,52 +11,105 @@ class ResourceLimits extends Component
|
||||||
|
|
||||||
public $resource;
|
public $resource;
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
|
public ?string $limitsCpus = null;
|
||||||
|
|
||||||
|
public ?string $limitsCpuset = null;
|
||||||
|
|
||||||
|
public ?int $limitsCpuShares = null;
|
||||||
|
|
||||||
|
public string $limitsMemory;
|
||||||
|
|
||||||
|
public string $limitsMemorySwap;
|
||||||
|
|
||||||
|
public int $limitsMemorySwappiness;
|
||||||
|
|
||||||
|
public string $limitsMemoryReservation;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'resource.limits_memory' => 'required|string',
|
'limitsMemory' => 'required|string',
|
||||||
'resource.limits_memory_swap' => 'required|string',
|
'limitsMemorySwap' => 'required|string',
|
||||||
'resource.limits_memory_swappiness' => 'required|integer|min:0|max:100',
|
'limitsMemorySwappiness' => 'required|integer|min:0|max:100',
|
||||||
'resource.limits_memory_reservation' => 'required|string',
|
'limitsMemoryReservation' => 'required|string',
|
||||||
'resource.limits_cpus' => 'nullable',
|
'limitsCpus' => 'nullable',
|
||||||
'resource.limits_cpuset' => 'nullable',
|
'limitsCpuset' => 'nullable',
|
||||||
'resource.limits_cpu_shares' => 'nullable',
|
'limitsCpuShares' => 'nullable',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'resource.limits_memory' => 'memory',
|
'limitsMemory' => 'memory',
|
||||||
'resource.limits_memory_swap' => 'swap',
|
'limitsMemorySwap' => 'swap',
|
||||||
'resource.limits_memory_swappiness' => 'swappiness',
|
'limitsMemorySwappiness' => 'swappiness',
|
||||||
'resource.limits_memory_reservation' => 'reservation',
|
'limitsMemoryReservation' => 'reservation',
|
||||||
'resource.limits_cpus' => 'cpus',
|
'limitsCpus' => 'cpus',
|
||||||
'resource.limits_cpuset' => 'cpuset',
|
'limitsCpuset' => 'cpuset',
|
||||||
'resource.limits_cpu_shares' => 'cpu shares',
|
'limitsCpuShares' => 'cpu shares',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// 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_memory = $this->limitsMemory;
|
||||||
|
$this->resource->limits_memory_swap = $this->limitsMemorySwap;
|
||||||
|
$this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness;
|
||||||
|
$this->resource->limits_memory_reservation = $this->limitsMemoryReservation;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->limitsCpus = $this->resource->limits_cpus;
|
||||||
|
$this->limitsCpuset = $this->resource->limits_cpuset;
|
||||||
|
$this->limitsCpuShares = $this->resource->limits_cpu_shares;
|
||||||
|
$this->limitsMemory = $this->resource->limits_memory;
|
||||||
|
$this->limitsMemorySwap = $this->resource->limits_memory_swap;
|
||||||
|
$this->limitsMemorySwappiness = $this->resource->limits_memory_swappiness;
|
||||||
|
$this->limitsMemoryReservation = $this->resource->limits_memory_reservation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->syncData(false);
|
||||||
|
}
|
||||||
|
|
||||||
public function submit()
|
public function submit()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
if (! $this->resource->limits_memory) {
|
|
||||||
$this->resource->limits_memory = '0';
|
// Apply default values to properties
|
||||||
|
if (! $this->limitsMemory) {
|
||||||
|
$this->limitsMemory = '0';
|
||||||
}
|
}
|
||||||
if (! $this->resource->limits_memory_swap) {
|
if (! $this->limitsMemorySwap) {
|
||||||
$this->resource->limits_memory_swap = '0';
|
$this->limitsMemorySwap = '0';
|
||||||
}
|
}
|
||||||
if (is_null($this->resource->limits_memory_swappiness)) {
|
if (is_null($this->limitsMemorySwappiness)) {
|
||||||
$this->resource->limits_memory_swappiness = '60';
|
$this->limitsMemorySwappiness = 60;
|
||||||
}
|
}
|
||||||
if (! $this->resource->limits_memory_reservation) {
|
if (! $this->limitsMemoryReservation) {
|
||||||
$this->resource->limits_memory_reservation = '0';
|
$this->limitsMemoryReservation = '0';
|
||||||
}
|
}
|
||||||
if (! $this->resource->limits_cpus) {
|
if (! $this->limitsCpus) {
|
||||||
$this->resource->limits_cpus = '0';
|
$this->limitsCpus = '0';
|
||||||
}
|
}
|
||||||
if ($this->resource->limits_cpuset === '') {
|
if ($this->limitsCpuset === '') {
|
||||||
$this->resource->limits_cpuset = null;
|
$this->limitsCpuset = null;
|
||||||
}
|
}
|
||||||
if (is_null($this->resource->limits_cpu_shares)) {
|
if (is_null($this->limitsCpuShares)) {
|
||||||
$this->resource->limits_cpu_shares = 1024;
|
$this->limitsCpuShares = 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
|
$this->syncData(true);
|
||||||
$this->resource->save();
|
$this->resource->save();
|
||||||
$this->dispatch('success', 'Resource limits updated.');
|
$this->dispatch('success', 'Resource limits updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
|
||||||
|
|
@ -25,20 +25,48 @@ class Show extends Component
|
||||||
|
|
||||||
public ?string $startedAt = null;
|
public ?string $startedAt = null;
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
public string $mountPath;
|
||||||
|
|
||||||
|
public ?string $hostPath = null;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'storage.name' => 'required|string',
|
'name' => 'required|string',
|
||||||
'storage.mount_path' => 'required|string',
|
'mountPath' => 'required|string',
|
||||||
'storage.host_path' => 'string|nullable',
|
'hostPath' => 'string|nullable',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'name' => 'name',
|
'name' => 'name',
|
||||||
'mount_path' => 'mount',
|
'mountPath' => 'mount',
|
||||||
'host_path' => 'host',
|
'hostPath' => 'host',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// Sync TO model (before save)
|
||||||
|
$this->storage->name = $this->name;
|
||||||
|
$this->storage->mount_path = $this->mountPath;
|
||||||
|
$this->storage->host_path = $this->hostPath;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->name = $this->storage->name;
|
||||||
|
$this->mountPath = $this->storage->mount_path;
|
||||||
|
$this->hostPath = $this->storage->host_path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
$this->syncData(false);
|
||||||
$this->isReadOnly = $this->storage->isReadOnlyVolume();
|
$this->isReadOnly = $this->storage->isReadOnlyVolume();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +75,7 @@ public function submit()
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
|
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
$this->syncData(true);
|
||||||
$this->storage->save();
|
$this->storage->save();
|
||||||
$this->dispatch('success', 'Storage updated successfully');
|
$this->dispatch('success', 'Storage updated successfully');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,24 @@ class Show extends Component
|
||||||
|
|
||||||
public PrivateKey $private_key;
|
public PrivateKey $private_key;
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
|
public string $privateKeyValue;
|
||||||
|
|
||||||
|
public bool $isGitRelated = false;
|
||||||
|
|
||||||
public $public_key = 'Loading...';
|
public $public_key = 'Loading...';
|
||||||
|
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'private_key.name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'private_key.description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'private_key.private_key' => 'required|string',
|
'privateKeyValue' => 'required|string',
|
||||||
'private_key.is_git_related' => 'nullable|boolean',
|
'isGitRelated' => 'nullable|boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,25 +39,48 @@ protected function messages(): array
|
||||||
return array_merge(
|
return array_merge(
|
||||||
ValidationPatterns::combinedMessages(),
|
ValidationPatterns::combinedMessages(),
|
||||||
[
|
[
|
||||||
'private_key.name.required' => 'The Name field is required.',
|
'name.required' => 'The Name field is required.',
|
||||||
'private_key.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||||
'private_key.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||||
'private_key.private_key.required' => 'The Private Key field is required.',
|
'privateKeyValue.required' => 'The Private Key field is required.',
|
||||||
'private_key.private_key.string' => 'The Private Key must be a valid string.',
|
'privateKeyValue.string' => 'The Private Key must be a valid string.',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'private_key.name' => 'name',
|
'name' => 'name',
|
||||||
'private_key.description' => 'description',
|
'description' => 'description',
|
||||||
'private_key.private_key' => 'private key',
|
'privateKeyValue' => 'private key',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// Sync TO model (before save)
|
||||||
|
$this->private_key->name = $this->name;
|
||||||
|
$this->private_key->description = $this->description;
|
||||||
|
$this->private_key->private_key = $this->privateKeyValue;
|
||||||
|
$this->private_key->is_git_related = $this->isGitRelated;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->name = $this->private_key->name;
|
||||||
|
$this->description = $this->private_key->description;
|
||||||
|
$this->privateKeyValue = $this->private_key->private_key;
|
||||||
|
$this->isGitRelated = $this->private_key->is_git_related;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
|
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
|
||||||
|
$this->syncData(false);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +113,10 @@ public function changePrivateKey()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->private_key);
|
$this->authorize('update', $this->private_key);
|
||||||
|
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
$this->syncData(true);
|
||||||
$this->private_key->updatePrivateKey([
|
$this->private_key->updatePrivateKey([
|
||||||
'private_key' => formatPrivateKey($this->private_key->private_key),
|
'private_key' => formatPrivateKey($this->private_key->private_key),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ private function loadHetznerData(string $token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCpuVendorInfo(array $serverType): string|null
|
private function getCpuVendorInfo(array $serverType): ?string
|
||||||
{
|
{
|
||||||
$name = strtolower($serverType['name'] ?? '');
|
$name = strtolower($serverType['name'] ?? '');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ class Proxy extends Component
|
||||||
|
|
||||||
public ?string $redirectUrl = null;
|
public ?string $redirectUrl = null;
|
||||||
|
|
||||||
|
public bool $generateExactLabels = false;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$teamId = auth()->user()->currentTeam()->id;
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
|
|
@ -33,7 +35,7 @@ public function getListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'server.settings.generate_exact_labels' => 'required|boolean',
|
'generateExactLabels' => 'required|boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
|
|
@ -41,6 +43,16 @@ public function mount()
|
||||||
$this->selectedProxy = $this->server->proxyType();
|
$this->selectedProxy = $this->server->proxyType();
|
||||||
$this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true);
|
$this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true);
|
||||||
$this->redirectUrl = data_get($this->server, 'proxy.redirect_url');
|
$this->redirectUrl = data_get($this->server, 'proxy.redirect_url');
|
||||||
|
$this->syncData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
$this->server->settings->generate_exact_labels = $this->generateExactLabels;
|
||||||
|
} else {
|
||||||
|
$this->generateExactLabels = $this->server->settings->generate_exact_labels ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConfigurationFilePathProperty()
|
public function getConfigurationFilePathProperty()
|
||||||
|
|
@ -75,6 +87,7 @@ public function instantSave()
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->server);
|
$this->authorize('update', $this->server);
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
$this->syncData(true);
|
||||||
$this->server->settings->save();
|
$this->server->settings->save();
|
||||||
$this->dispatch('success', 'Settings saved.');
|
$this->dispatch('success', 'Settings saved.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
|
||||||
|
|
@ -34,32 +34,60 @@ class Change extends Component
|
||||||
|
|
||||||
public ?GithubApp $github_app = null;
|
public ?GithubApp $github_app = null;
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
public string $name;
|
public string $name;
|
||||||
|
|
||||||
public bool $is_system_wide;
|
public ?string $organization = null;
|
||||||
|
|
||||||
|
public string $apiUrl;
|
||||||
|
|
||||||
|
public string $htmlUrl;
|
||||||
|
|
||||||
|
public string $customUser;
|
||||||
|
|
||||||
|
public int $customPort;
|
||||||
|
|
||||||
|
public int $appId;
|
||||||
|
|
||||||
|
public int $installationId;
|
||||||
|
|
||||||
|
public string $clientId;
|
||||||
|
|
||||||
|
public string $clientSecret;
|
||||||
|
|
||||||
|
public string $webhookSecret;
|
||||||
|
|
||||||
|
public bool $isSystemWide;
|
||||||
|
|
||||||
|
public int $privateKeyId;
|
||||||
|
|
||||||
|
public ?string $contents = null;
|
||||||
|
|
||||||
|
public ?string $metadata = null;
|
||||||
|
|
||||||
|
public ?string $pullRequests = null;
|
||||||
|
|
||||||
public $applications;
|
public $applications;
|
||||||
|
|
||||||
public $privateKeys;
|
public $privateKeys;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'github_app.name' => 'required|string',
|
'name' => 'required|string',
|
||||||
'github_app.organization' => 'nullable|string',
|
'organization' => 'nullable|string',
|
||||||
'github_app.api_url' => 'required|string',
|
'apiUrl' => 'required|string',
|
||||||
'github_app.html_url' => 'required|string',
|
'htmlUrl' => 'required|string',
|
||||||
'github_app.custom_user' => 'required|string',
|
'customUser' => 'required|string',
|
||||||
'github_app.custom_port' => 'required|int',
|
'customPort' => 'required|int',
|
||||||
'github_app.app_id' => 'required|int',
|
'appId' => 'required|int',
|
||||||
'github_app.installation_id' => 'required|int',
|
'installationId' => 'required|int',
|
||||||
'github_app.client_id' => 'required|string',
|
'clientId' => 'required|string',
|
||||||
'github_app.client_secret' => 'required|string',
|
'clientSecret' => 'required|string',
|
||||||
'github_app.webhook_secret' => 'required|string',
|
'webhookSecret' => 'required|string',
|
||||||
'github_app.is_system_wide' => 'required|bool',
|
'isSystemWide' => 'required|bool',
|
||||||
'github_app.contents' => 'nullable|string',
|
'contents' => 'nullable|string',
|
||||||
'github_app.metadata' => 'nullable|string',
|
'metadata' => 'nullable|string',
|
||||||
'github_app.pull_requests' => 'nullable|string',
|
'pullRequests' => 'nullable|string',
|
||||||
'github_app.administration' => 'nullable|string',
|
'privateKeyId' => 'required|int',
|
||||||
'github_app.private_key_id' => 'required|int',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function boot()
|
public function boot()
|
||||||
|
|
@ -69,6 +97,52 @@ public function boot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// Sync TO model (before save)
|
||||||
|
$this->github_app->name = $this->name;
|
||||||
|
$this->github_app->organization = $this->organization;
|
||||||
|
$this->github_app->api_url = $this->apiUrl;
|
||||||
|
$this->github_app->html_url = $this->htmlUrl;
|
||||||
|
$this->github_app->custom_user = $this->customUser;
|
||||||
|
$this->github_app->custom_port = $this->customPort;
|
||||||
|
$this->github_app->app_id = $this->appId;
|
||||||
|
$this->github_app->installation_id = $this->installationId;
|
||||||
|
$this->github_app->client_id = $this->clientId;
|
||||||
|
$this->github_app->client_secret = $this->clientSecret;
|
||||||
|
$this->github_app->webhook_secret = $this->webhookSecret;
|
||||||
|
$this->github_app->is_system_wide = $this->isSystemWide;
|
||||||
|
$this->github_app->private_key_id = $this->privateKeyId;
|
||||||
|
$this->github_app->contents = $this->contents;
|
||||||
|
$this->github_app->metadata = $this->metadata;
|
||||||
|
$this->github_app->pull_requests = $this->pullRequests;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->name = $this->github_app->name;
|
||||||
|
$this->organization = $this->github_app->organization;
|
||||||
|
$this->apiUrl = $this->github_app->api_url;
|
||||||
|
$this->htmlUrl = $this->github_app->html_url;
|
||||||
|
$this->customUser = $this->github_app->custom_user;
|
||||||
|
$this->customPort = $this->github_app->custom_port;
|
||||||
|
$this->appId = $this->github_app->app_id;
|
||||||
|
$this->installationId = $this->github_app->installation_id;
|
||||||
|
$this->clientId = $this->github_app->client_id;
|
||||||
|
$this->clientSecret = $this->github_app->client_secret;
|
||||||
|
$this->webhookSecret = $this->github_app->webhook_secret;
|
||||||
|
$this->isSystemWide = $this->github_app->is_system_wide;
|
||||||
|
$this->privateKeyId = $this->github_app->private_key_id;
|
||||||
|
$this->contents = $this->github_app->contents;
|
||||||
|
$this->metadata = $this->github_app->metadata;
|
||||||
|
$this->pullRequests = $this->github_app->pull_requests;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function checkPermissions()
|
public function checkPermissions()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -126,6 +200,10 @@ public function mount()
|
||||||
$this->applications = $this->github_app->applications;
|
$this->applications = $this->github_app->applications;
|
||||||
$settings = instanceSettings();
|
$settings = instanceSettings();
|
||||||
|
|
||||||
|
// Sync data from model to properties
|
||||||
|
$this->syncData(false);
|
||||||
|
|
||||||
|
// Override name with kebab case for display
|
||||||
$this->name = str($this->github_app->name)->kebab();
|
$this->name = str($this->github_app->name)->kebab();
|
||||||
$this->fqdn = $settings->fqdn;
|
$this->fqdn = $settings->fqdn;
|
||||||
|
|
||||||
|
|
@ -247,21 +325,9 @@ public function submit()
|
||||||
$this->authorize('update', $this->github_app);
|
$this->authorize('update', $this->github_app);
|
||||||
|
|
||||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||||
$this->validate([
|
$this->validate();
|
||||||
'github_app.name' => 'required|string',
|
|
||||||
'github_app.organization' => 'nullable|string',
|
$this->syncData(true);
|
||||||
'github_app.api_url' => 'required|string',
|
|
||||||
'github_app.html_url' => 'required|string',
|
|
||||||
'github_app.custom_user' => 'required|string',
|
|
||||||
'github_app.custom_port' => 'required|int',
|
|
||||||
'github_app.app_id' => 'required|int',
|
|
||||||
'github_app.installation_id' => 'required|int',
|
|
||||||
'github_app.client_id' => 'required|string',
|
|
||||||
'github_app.client_secret' => 'required|string',
|
|
||||||
'github_app.webhook_secret' => 'required|string',
|
|
||||||
'github_app.is_system_wide' => 'required|bool',
|
|
||||||
'github_app.private_key_id' => 'required|int',
|
|
||||||
]);
|
|
||||||
$this->github_app->save();
|
$this->github_app->save();
|
||||||
$this->dispatch('success', 'Github App updated.');
|
$this->dispatch('success', 'Github App updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
@ -286,6 +352,8 @@ public function instantSave()
|
||||||
$this->authorize('update', $this->github_app);
|
$this->authorize('update', $this->github_app);
|
||||||
|
|
||||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||||
|
|
||||||
|
$this->syncData(true);
|
||||||
$this->github_app->save();
|
$this->github_app->save();
|
||||||
$this->dispatch('success', 'Github App updated.');
|
$this->dispatch('success', 'Github App updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,34 @@ class Form extends Component
|
||||||
|
|
||||||
public S3Storage $storage;
|
public S3Storage $storage;
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
|
public ?string $name = null;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
|
public string $endpoint;
|
||||||
|
|
||||||
|
public string $bucket;
|
||||||
|
|
||||||
|
public string $region;
|
||||||
|
|
||||||
|
public string $key;
|
||||||
|
|
||||||
|
public string $secret;
|
||||||
|
|
||||||
|
public ?bool $isUsable = null;
|
||||||
|
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'storage.is_usable' => 'nullable|boolean',
|
'isUsable' => 'nullable|boolean',
|
||||||
'storage.name' => ValidationPatterns::nameRules(required: false),
|
'name' => ValidationPatterns::nameRules(required: false),
|
||||||
'storage.description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
'storage.region' => 'required|max:255',
|
'region' => 'required|max:255',
|
||||||
'storage.key' => 'required|max:255',
|
'key' => 'required|max:255',
|
||||||
'storage.secret' => 'required|max:255',
|
'secret' => 'required|max:255',
|
||||||
'storage.bucket' => 'required|max:255',
|
'bucket' => 'required|max:255',
|
||||||
'storage.endpoint' => 'required|url|max:255',
|
'endpoint' => 'required|url|max:255',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,34 +50,69 @@ protected function messages(): array
|
||||||
return array_merge(
|
return array_merge(
|
||||||
ValidationPatterns::combinedMessages(),
|
ValidationPatterns::combinedMessages(),
|
||||||
[
|
[
|
||||||
'storage.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||||
'storage.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||||
'storage.region.required' => 'The Region field is required.',
|
'region.required' => 'The Region field is required.',
|
||||||
'storage.region.max' => 'The Region may not be greater than 255 characters.',
|
'region.max' => 'The Region may not be greater than 255 characters.',
|
||||||
'storage.key.required' => 'The Access Key field is required.',
|
'key.required' => 'The Access Key field is required.',
|
||||||
'storage.key.max' => 'The Access Key may not be greater than 255 characters.',
|
'key.max' => 'The Access Key may not be greater than 255 characters.',
|
||||||
'storage.secret.required' => 'The Secret Key field is required.',
|
'secret.required' => 'The Secret Key field is required.',
|
||||||
'storage.secret.max' => 'The Secret Key may not be greater than 255 characters.',
|
'secret.max' => 'The Secret Key may not be greater than 255 characters.',
|
||||||
'storage.bucket.required' => 'The Bucket field is required.',
|
'bucket.required' => 'The Bucket field is required.',
|
||||||
'storage.bucket.max' => 'The Bucket may not be greater than 255 characters.',
|
'bucket.max' => 'The Bucket may not be greater than 255 characters.',
|
||||||
'storage.endpoint.required' => 'The Endpoint field is required.',
|
'endpoint.required' => 'The Endpoint field is required.',
|
||||||
'storage.endpoint.url' => 'The Endpoint must be a valid URL.',
|
'endpoint.url' => 'The Endpoint must be a valid URL.',
|
||||||
'storage.endpoint.max' => 'The Endpoint may not be greater than 255 characters.',
|
'endpoint.max' => 'The Endpoint may not be greater than 255 characters.',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'storage.is_usable' => 'Is Usable',
|
'isUsable' => 'Is Usable',
|
||||||
'storage.name' => 'Name',
|
'name' => 'Name',
|
||||||
'storage.description' => 'Description',
|
'description' => 'Description',
|
||||||
'storage.region' => 'Region',
|
'region' => 'Region',
|
||||||
'storage.key' => 'Key',
|
'key' => 'Key',
|
||||||
'storage.secret' => 'Secret',
|
'secret' => 'Secret',
|
||||||
'storage.bucket' => 'Bucket',
|
'bucket' => 'Bucket',
|
||||||
'storage.endpoint' => 'Endpoint',
|
'endpoint' => 'Endpoint',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// Sync TO model (before save)
|
||||||
|
$this->storage->name = $this->name;
|
||||||
|
$this->storage->description = $this->description;
|
||||||
|
$this->storage->endpoint = $this->endpoint;
|
||||||
|
$this->storage->bucket = $this->bucket;
|
||||||
|
$this->storage->region = $this->region;
|
||||||
|
$this->storage->key = $this->key;
|
||||||
|
$this->storage->secret = $this->secret;
|
||||||
|
$this->storage->is_usable = $this->isUsable;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->name = $this->storage->name;
|
||||||
|
$this->description = $this->storage->description;
|
||||||
|
$this->endpoint = $this->storage->endpoint;
|
||||||
|
$this->bucket = $this->storage->bucket;
|
||||||
|
$this->region = $this->storage->region;
|
||||||
|
$this->key = $this->storage->key;
|
||||||
|
$this->secret = $this->storage->secret;
|
||||||
|
$this->isUsable = $this->storage->is_usable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->syncData(false);
|
||||||
|
}
|
||||||
|
|
||||||
public function testConnection()
|
public function testConnection()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -94,6 +146,9 @@ public function submit()
|
||||||
|
|
||||||
DB::transaction(function () {
|
DB::transaction(function () {
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
|
// Sync properties to model before saving
|
||||||
|
$this->syncData(true);
|
||||||
$this->storage->save();
|
$this->storage->save();
|
||||||
|
|
||||||
// Test connection with new values - if this fails, transaction will rollback
|
// Test connection with new values - if this fails, transaction will rollback
|
||||||
|
|
@ -103,12 +158,16 @@ public function submit()
|
||||||
$this->storage->is_usable = true;
|
$this->storage->is_usable = true;
|
||||||
$this->storage->unusable_email_sent = false;
|
$this->storage->unusable_email_sent = false;
|
||||||
$this->storage->save();
|
$this->storage->save();
|
||||||
|
|
||||||
|
// Update local property to reflect success
|
||||||
|
$this->isUsable = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->dispatch('success', 'Storage settings updated and connection verified.');
|
$this->dispatch('success', 'Storage settings updated and connection verified.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Refresh the model to revert UI to database values after rollback
|
// Refresh the model to revert UI to database values after rollback
|
||||||
$this->storage->refresh();
|
$this->storage->refresh();
|
||||||
|
$this->syncData(false);
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,16 @@ class Index extends Component
|
||||||
|
|
||||||
public Team $team;
|
public Team $team;
|
||||||
|
|
||||||
|
// Explicit properties
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
public ?string $description = null;
|
||||||
|
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'team.name' => ValidationPatterns::nameRules(),
|
'name' => ValidationPatterns::nameRules(),
|
||||||
'team.description' => ValidationPatterns::descriptionRules(),
|
'description' => ValidationPatterns::descriptionRules(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,21 +36,40 @@ protected function messages(): array
|
||||||
return array_merge(
|
return array_merge(
|
||||||
ValidationPatterns::combinedMessages(),
|
ValidationPatterns::combinedMessages(),
|
||||||
[
|
[
|
||||||
'team.name.required' => 'The Name field is required.',
|
'name.required' => 'The Name field is required.',
|
||||||
'team.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||||
'team.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'team.name' => 'name',
|
'name' => 'name',
|
||||||
'team.description' => 'description',
|
'description' => 'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data between component properties and model
|
||||||
|
*
|
||||||
|
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
|
||||||
|
*/
|
||||||
|
private function syncData(bool $toModel = false): void
|
||||||
|
{
|
||||||
|
if ($toModel) {
|
||||||
|
// Sync TO model (before save)
|
||||||
|
$this->team->name = $this->name;
|
||||||
|
$this->team->description = $this->description;
|
||||||
|
} else {
|
||||||
|
// Sync FROM model (on load/refresh)
|
||||||
|
$this->name = $this->team->name;
|
||||||
|
$this->description = $this->team->description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->team = currentTeam();
|
$this->team = currentTeam();
|
||||||
|
$this->syncData(false);
|
||||||
|
|
||||||
if (auth()->user()->isAdminFromSession()) {
|
if (auth()->user()->isAdminFromSession()) {
|
||||||
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
|
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
|
||||||
|
|
@ -62,6 +86,7 @@ public function submit()
|
||||||
$this->validate();
|
$this->validate();
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->team);
|
$this->authorize('update', $this->team);
|
||||||
|
$this->syncData(true);
|
||||||
$this->team->save();
|
$this->team->save();
|
||||||
refreshSession();
|
refreshSession();
|
||||||
$this->dispatch('success', 'Team updated.');
|
$this->dispatch('success', 'Team updated.');
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,14 @@
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\View\Component;
|
use Illuminate\View\Component;
|
||||||
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
class Checkbox extends Component
|
class Checkbox extends Component
|
||||||
{
|
{
|
||||||
|
public ?string $modelBinding = null;
|
||||||
|
|
||||||
|
public ?string $htmlId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new component instance.
|
* Create a new component instance.
|
||||||
*/
|
*/
|
||||||
|
|
@ -47,6 +52,18 @@ public function __construct(
|
||||||
*/
|
*/
|
||||||
public function render(): View|Closure|string
|
public function render(): View|Closure|string
|
||||||
{
|
{
|
||||||
|
// Store original ID for wire:model binding (property name)
|
||||||
|
$this->modelBinding = $this->id;
|
||||||
|
|
||||||
|
// Generate unique HTML ID by adding random suffix
|
||||||
|
// This prevents duplicate IDs when multiple forms are on the same page
|
||||||
|
if ($this->id) {
|
||||||
|
$uniqueSuffix = new Cuid2;
|
||||||
|
$this->htmlId = $this->id.'-'.$uniqueSuffix;
|
||||||
|
} else {
|
||||||
|
$this->htmlId = $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
return view('components.forms.checkbox');
|
return view('components.forms.checkbox');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
|
|
||||||
class Datalist extends Component
|
class Datalist extends Component
|
||||||
{
|
{
|
||||||
|
public ?string $modelBinding = null;
|
||||||
|
|
||||||
|
public ?string $htmlId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new component instance.
|
* Create a new component instance.
|
||||||
*/
|
*/
|
||||||
|
|
@ -47,11 +51,27 @@ public function __construct(
|
||||||
*/
|
*/
|
||||||
public function render(): View|Closure|string
|
public function render(): View|Closure|string
|
||||||
{
|
{
|
||||||
|
// Store original ID for wire:model binding (property name)
|
||||||
|
$this->modelBinding = $this->id;
|
||||||
|
|
||||||
if (is_null($this->id)) {
|
if (is_null($this->id)) {
|
||||||
$this->id = new Cuid2;
|
$this->id = new Cuid2;
|
||||||
|
// Don't create wire:model binding for auto-generated IDs
|
||||||
|
$this->modelBinding = 'null';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate unique HTML ID by adding random suffix
|
||||||
|
// This prevents duplicate IDs when multiple forms are on the same page
|
||||||
|
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||||
|
// Use original ID with random suffix for uniqueness
|
||||||
|
$uniqueSuffix = new Cuid2;
|
||||||
|
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||||
|
} else {
|
||||||
|
$this->htmlId = (string) $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_null($this->name)) {
|
if (is_null($this->name)) {
|
||||||
$this->name = $this->id;
|
$this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('components.forms.datalist');
|
return view('components.forms.datalist');
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
|
|
||||||
class Input extends Component
|
class Input extends Component
|
||||||
{
|
{
|
||||||
|
public ?string $modelBinding = null;
|
||||||
|
|
||||||
|
public ?string $htmlId = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public ?string $id = null,
|
public ?string $id = null,
|
||||||
public ?string $name = null,
|
public ?string $name = null,
|
||||||
|
|
@ -43,11 +47,26 @@ public function __construct(
|
||||||
|
|
||||||
public function render(): View|Closure|string
|
public function render(): View|Closure|string
|
||||||
{
|
{
|
||||||
|
// Store original ID for wire:model binding (property name)
|
||||||
|
$this->modelBinding = $this->id;
|
||||||
|
|
||||||
if (is_null($this->id)) {
|
if (is_null($this->id)) {
|
||||||
$this->id = new Cuid2;
|
$this->id = new Cuid2;
|
||||||
|
// Don't create wire:model binding for auto-generated IDs
|
||||||
|
$this->modelBinding = 'null';
|
||||||
}
|
}
|
||||||
|
// Generate unique HTML ID by adding random suffix
|
||||||
|
// This prevents duplicate IDs when multiple forms are on the same page
|
||||||
|
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||||
|
// Use original ID with random suffix for uniqueness
|
||||||
|
$uniqueSuffix = new Cuid2;
|
||||||
|
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||||
|
} else {
|
||||||
|
$this->htmlId = (string) $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_null($this->name)) {
|
if (is_null($this->name)) {
|
||||||
$this->name = $this->id;
|
$this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
|
||||||
}
|
}
|
||||||
if ($this->type === 'password') {
|
if ($this->type === 'password') {
|
||||||
$this->defaultClass = $this->defaultClass.' pr-[2.8rem]';
|
$this->defaultClass = $this->defaultClass.' pr-[2.8rem]';
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
|
|
||||||
class Select extends Component
|
class Select extends Component
|
||||||
{
|
{
|
||||||
|
public ?string $modelBinding = null;
|
||||||
|
|
||||||
|
public ?string $htmlId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new component instance.
|
* Create a new component instance.
|
||||||
*/
|
*/
|
||||||
|
|
@ -40,11 +44,27 @@ public function __construct(
|
||||||
*/
|
*/
|
||||||
public function render(): View|Closure|string
|
public function render(): View|Closure|string
|
||||||
{
|
{
|
||||||
|
// Store original ID for wire:model binding (property name)
|
||||||
|
$this->modelBinding = $this->id;
|
||||||
|
|
||||||
if (is_null($this->id)) {
|
if (is_null($this->id)) {
|
||||||
$this->id = new Cuid2;
|
$this->id = new Cuid2;
|
||||||
|
// Don't create wire:model binding for auto-generated IDs
|
||||||
|
$this->modelBinding = 'null';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate unique HTML ID by adding random suffix
|
||||||
|
// This prevents duplicate IDs when multiple forms are on the same page
|
||||||
|
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||||
|
// Use original ID with random suffix for uniqueness
|
||||||
|
$uniqueSuffix = new Cuid2;
|
||||||
|
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||||
|
} else {
|
||||||
|
$this->htmlId = (string) $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_null($this->name)) {
|
if (is_null($this->name)) {
|
||||||
$this->name = $this->id;
|
$this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('components.forms.select');
|
return view('components.forms.select');
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
|
|
||||||
class Textarea extends Component
|
class Textarea extends Component
|
||||||
{
|
{
|
||||||
|
public ?string $modelBinding = null;
|
||||||
|
|
||||||
|
public ?string $htmlId = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new component instance.
|
* Create a new component instance.
|
||||||
*/
|
*/
|
||||||
|
|
@ -54,11 +58,27 @@ public function __construct(
|
||||||
*/
|
*/
|
||||||
public function render(): View|Closure|string
|
public function render(): View|Closure|string
|
||||||
{
|
{
|
||||||
|
// Store original ID for wire:model binding (property name)
|
||||||
|
$this->modelBinding = $this->id;
|
||||||
|
|
||||||
if (is_null($this->id)) {
|
if (is_null($this->id)) {
|
||||||
$this->id = new Cuid2;
|
$this->id = new Cuid2;
|
||||||
|
// Don't create wire:model binding for auto-generated IDs
|
||||||
|
$this->modelBinding = 'null';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate unique HTML ID by adding random suffix
|
||||||
|
// This prevents duplicate IDs when multiple forms are on the same page
|
||||||
|
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||||
|
// Use original ID with random suffix for uniqueness
|
||||||
|
$uniqueSuffix = new Cuid2;
|
||||||
|
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||||
|
} else {
|
||||||
|
$this->htmlId = (string) $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_null($this->name)) {
|
if (is_null($this->name)) {
|
||||||
$this->name = $this->id;
|
$this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// $this->label = Str::title($this->label);
|
// $this->label = Str::title($this->label);
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'legacy_model_binding' => true,
|
'legacy_model_binding' => false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|---------------------------------------------------------------------------
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,14 @@
|
||||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||||
wire:loading.attr="disabled"
|
wire:loading.attr="disabled"
|
||||||
wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||||
wire:model={{ $id }} @if ($checked) checked @endif />
|
wire:model={{ $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||||
@else
|
@else
|
||||||
@if ($domValue)
|
@if ($domValue)
|
||||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||||
value={{ $domValue }} @if ($checked) checked @endif />
|
value={{ $domValue }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||||
@else
|
@else
|
||||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||||
wire:model={{ $value ?? $id }} @if ($checked) checked @endif />
|
wire:model={{ $value ?? $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<div x-data="{
|
<div x-data="{
|
||||||
open: false,
|
open: false,
|
||||||
search: '',
|
search: '',
|
||||||
selected: @entangle($id).live,
|
selected: @entangle($modelBinding).live,
|
||||||
options: [],
|
options: [],
|
||||||
filteredOptions: [],
|
filteredOptions: [],
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:
|
||||||
<div x-data="{
|
<div x-data="{
|
||||||
open: false,
|
open: false,
|
||||||
search: '',
|
search: '',
|
||||||
selected: @entangle(($attributes->whereStartsWith('wire:model')->first() ? $attributes->wire('model')->value() : $id)).live,
|
selected: @entangle(($attributes->whereStartsWith('wire:model')->first() ? $attributes->wire('model')->value() : $modelBinding)).live,
|
||||||
options: [],
|
options: [],
|
||||||
filteredOptions: [],
|
filteredOptions: [],
|
||||||
|
|
||||||
|
|
@ -284,7 +284,7 @@ class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200"
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@error($id)
|
@error($modelBinding)
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
||||||
@endif
|
@endif
|
||||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||||
@if ($id !== 'null') wire:model={{ $id }} @endif
|
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" wire:loading.attr="disabled"
|
wire:loading.attr="disabled"
|
||||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
|
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||||
|
|
@ -38,19 +38,19 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
||||||
@else
|
@else
|
||||||
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
|
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
|
||||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
|
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
|
||||||
@if ($id !== 'null') wire:model={{ $id }} @endif
|
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" wire:loading.attr="disabled"
|
wire:loading.attr="disabled"
|
||||||
type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}"
|
type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}"
|
||||||
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
|
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
|
||||||
maxlength="{{ $attributes->get('maxlength') }}"
|
maxlength="{{ $attributes->get('maxlength') }}"
|
||||||
@if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}"
|
@if ($htmlId !== 'null') id={{ $htmlId }} @endif name="{{ $name }}"
|
||||||
placeholder="{{ $attributes->get('placeholder') }}"
|
placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||||
@endif
|
@endif
|
||||||
@if (!$label && $helper)
|
@if (!$label && $helper)
|
||||||
<x-helper :helper="$helper" />
|
<x-helper :helper="$helper" />
|
||||||
@endif
|
@endif
|
||||||
@error($id)
|
@error($modelBinding)
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ class="flex gap-1 items-center mb-1 text-sm font-medium {{ $disabled ? 'text-neu
|
||||||
</label>
|
</label>
|
||||||
@endif
|
@endif
|
||||||
<select {{ $attributes->merge(['class' => $defaultClass]) }} @disabled($disabled) @required($required)
|
<select {{ $attributes->merge(['class' => $defaultClass]) }} @disabled($disabled) @required($required)
|
||||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" wire:loading.attr="disabled" name={{ $id }}
|
wire:loading.attr="disabled" name={{ $modelBinding }} id="{{ $htmlId }}"
|
||||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif>
|
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @else wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif>
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</select>
|
</select>
|
||||||
@error($id)
|
@error($modelBinding)
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ function handleKeydown(e) {
|
||||||
</label>
|
</label>
|
||||||
@endif
|
@endif
|
||||||
@if ($useMonacoEditor)
|
@if ($useMonacoEditor)
|
||||||
<x-forms.monaco-editor id="{{ $id }}" language="{{ $monacoEditorLanguage }}" name="{{ $name }}"
|
<x-forms.monaco-editor id="{{ $modelBinding }}" language="{{ $monacoEditorLanguage }}" name="{{ $name }}"
|
||||||
name="{{ $id }}" model="{{ $value ?? $id }}" wire:model="{{ $value ?? $id }}"
|
name="{{ $modelBinding }}" model="{{ $value ?? $modelBinding }}" wire:model="{{ $value ?? $modelBinding }}"
|
||||||
readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" />
|
readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" />
|
||||||
@else
|
@else
|
||||||
@if ($type === 'password')
|
@if ($type === 'password')
|
||||||
|
|
@ -45,19 +45,18 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
|
||||||
@endif
|
@endif
|
||||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||||
@if ($id !== 'null') wire:model={{ $id }} @endif
|
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" wire:loading.attr="disabled"
|
wire:loading.attr="disabled"
|
||||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
|
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
||||||
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
|
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||||
@else
|
@else
|
||||||
wire:model={{ $value ?? $id }}
|
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}"
|
name="{{ $name }}" name={{ $modelBinding }}
|
||||||
name="{{ $name }}" name={{ $id }}
|
|
||||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -65,16 +64,15 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
|
||||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
|
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
|
||||||
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
|
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
|
||||||
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
|
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
|
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||||
@else
|
@else
|
||||||
wire:model={{ $value ?? $id }}
|
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}"
|
name="{{ $name }}" name={{ $modelBinding }}
|
||||||
name="{{ $name }}" name={{ $id }}
|
|
||||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@error($id)
|
@error($modelBinding)
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@
|
||||||
<div>General configuration for your application.</div>
|
<div>General configuration for your application.</div>
|
||||||
<div class="flex flex-col gap-2 py-4">
|
<div class="flex flex-col gap-2 py-4">
|
||||||
<div class="flex flex-col items-end gap-2 xl:flex-row">
|
<div class="flex flex-col items-end gap-2 xl:flex-row">
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()" id="application.name" label="Name" required />
|
<x-forms.input x-bind:disabled="shouldDisable()" id="name" label="Name" required />
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()" id="application.description" label="Description" />
|
<x-forms.input x-bind:disabled="shouldDisable()" id="description" label="Description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
|
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="application.build_pack"
|
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="build_pack"
|
||||||
label="Build Pack" required>
|
label="Build Pack" required>
|
||||||
<option value="nixpacks">Nixpacks</option>
|
<option value="nixpacks">Nixpacks</option>
|
||||||
<option value="static">Static</option>
|
<option value="static">Static</option>
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
<option value="dockercompose">Docker Compose</option>
|
<option value="dockercompose">Docker Compose</option>
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||||
<x-forms.select x-bind:disabled="!canUpdate" id="application.static_image"
|
<x-forms.select x-bind:disabled="!canUpdate" id="static_image"
|
||||||
label="Static Image" required>
|
label="Static Image" required>
|
||||||
<option value="nginx:alpine">nginx:alpine</option>
|
<option value="nginx:alpine">nginx:alpine</option>
|
||||||
<option disabled value="apache:alpine">apache:alpine</option>
|
<option disabled value="apache:alpine">apache:alpine</option>
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||||
<x-forms.textarea id="application.custom_nginx_configuration"
|
<x-forms.textarea id="custom_nginx_configuration"
|
||||||
placeholder="Empty means default configuration will be used." label="Custom Nginx Configuration"
|
placeholder="Empty means default configuration will be used." label="Custom Nginx Configuration"
|
||||||
helper="You can add custom Nginx configuration here." x-bind:disabled="!canUpdate" />
|
helper="You can add custom Nginx configuration here." x-bind:disabled="!canUpdate" />
|
||||||
@can('update', $application)
|
@can('update', $application)
|
||||||
|
|
@ -77,13 +77,13 @@
|
||||||
@endif
|
@endif
|
||||||
<div class="w-96 pb-6">
|
<div class="w-96 pb-6">
|
||||||
@if ($application->could_set_build_commands())
|
@if ($application->could_set_build_commands())
|
||||||
<x-forms.checkbox instantSave id="application.settings.is_static" label="Is it a static site?"
|
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?"
|
||||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this."
|
helper="If your application is a static site or the final build assets should be served as a static site, enable this."
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
@if ($application->settings->is_static && $application->build_pack !== 'static')
|
@if ($application->settings->is_static && $application->build_pack !== 'static')
|
||||||
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
|
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
|
||||||
helper="If your application is a SPA, enable this." id="application.settings.is_spa" instantSave
|
helper="If your application is a SPA, enable this." id="is_spa" instantSave
|
||||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<x-forms.select label="Direction" id="application.redirect" required
|
<x-forms.select label="Direction" id="redirect" required
|
||||||
helper="You must need to add www and non-www as an A DNS record. Make sure the www domain is added under Domains."
|
helper="You must need to add www and non-www as an A DNS record. Make sure the www domain is added under Domains."
|
||||||
x-bind:disabled="!canUpdate">
|
x-bind:disabled="!canUpdate">
|
||||||
<option value="both">Allow www & non-www.</option>
|
<option value="both">Allow www & non-www.</option>
|
||||||
|
|
@ -164,15 +164,15 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div class="flex flex-col gap-2 xl:flex-row">
|
<div class="flex flex-col gap-2 xl:flex-row">
|
||||||
@if ($application->build_pack === 'dockerimage')
|
@if ($application->build_pack === 'dockerimage')
|
||||||
@if ($application->destination->server->isSwarm())
|
@if ($application->destination->server->isSwarm())
|
||||||
<x-forms.input required id="application.docker_registry_image_name" label="Docker Image"
|
<x-forms.input required id="docker_registry_image_name" label="Docker Image"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag or Hash"
|
<x-forms.input id="docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@else
|
@else
|
||||||
<x-forms.input id="application.docker_registry_image_name" label="Docker Image"
|
<x-forms.input id="docker_registry_image_name" label="Docker Image"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag or Hash"
|
<x-forms.input id="docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -181,18 +181,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
$application->destination->server->isSwarm() ||
|
$application->destination->server->isSwarm() ||
|
||||||
$application->additional_servers->count() > 0 ||
|
$application->additional_servers->count() > 0 ||
|
||||||
$application->settings->is_build_server_enabled)
|
$application->settings->is_build_server_enabled)
|
||||||
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image"
|
<x-forms.input id="docker_registry_image_name" required label="Docker Image"
|
||||||
placeholder="Required!" x-bind:disabled="!canUpdate" />
|
placeholder="Required!" x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input id="application.docker_registry_image_tag"
|
<x-forms.input id="docker_registry_image_tag"
|
||||||
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
|
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
|
||||||
placeholder="Empty means latest will be used." label="Docker Image Tag"
|
placeholder="Empty means latest will be used." label="Docker Image Tag"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@else
|
@else
|
||||||
<x-forms.input id="application.docker_registry_image_name"
|
<x-forms.input id="docker_registry_image_name"
|
||||||
helper="Empty means it won't push the image to a docker registry. Pre-tag the image with your registry url if you want to push it to a private registry (default: Dockerhub). <br><br>Example: ghcr.io/myimage"
|
helper="Empty means it won't push the image to a docker registry. Pre-tag the image with your registry url if you want to push it to a private registry (default: Dockerhub). <br><br>Example: ghcr.io/myimage"
|
||||||
placeholder="Empty means it won't push the image to a docker registry."
|
placeholder="Empty means it won't push the image to a docker registry."
|
||||||
label="Docker Image" x-bind:disabled="!canUpdate" />
|
label="Docker Image" x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input id="application.docker_registry_image_tag"
|
<x-forms.input id="docker_registry_image_tag"
|
||||||
placeholder="Empty means only push commit sha tag."
|
placeholder="Empty means only push commit sha tag."
|
||||||
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
|
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
|
||||||
label="Docker Image Tag" x-bind:disabled="!canUpdate" />
|
label="Docker Image Tag" x-bind:disabled="!canUpdate" />
|
||||||
|
|
@ -206,20 +206,20 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<x-forms.input
|
<x-forms.input
|
||||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k --hostname=myapp"
|
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k --hostname=myapp"
|
||||||
id="application.custom_docker_run_options" label="Custom Docker Options"
|
id="custom_docker_run_options" label="Custom Docker Options"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@else
|
@else
|
||||||
@if ($application->could_set_build_commands())
|
@if ($application->could_set_build_commands())
|
||||||
@if ($application->build_pack === 'nixpacks')
|
@if ($application->build_pack === 'nixpacks')
|
||||||
<div class="flex flex-col gap-2 xl:flex-row">
|
<div class="flex flex-col gap-2 xl:flex-row">
|
||||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||||
id="application.install_command" label="Install Command"
|
id="install_command" label="Install Command"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||||
id="application.build_command" label="Build Command"
|
id="build_command" label="Build Command"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||||
id="application.start_command" label="Start Command"
|
id="start_command" label="Start Command"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-1 text-xs">Nixpacks will detect the required configuration
|
<div class="pt-1 text-xs">Nixpacks will detect the required configuration
|
||||||
|
|
@ -239,16 +239,16 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
@endcan
|
@endcan
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/"
|
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/"
|
||||||
id="application.base_directory" label="Base Directory"
|
id="base_directory" label="Base Directory"
|
||||||
helper="Directory to use as root. Useful for monorepos." />
|
helper="Directory to use as root. Useful for monorepos." />
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||||
placeholder="/docker-compose.yaml"
|
placeholder="/docker-compose.yaml"
|
||||||
id="application.docker_compose_location" label="Docker Compose Location"
|
id="docker_compose_location" label="Docker Compose Location"
|
||||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
|
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox instantSave
|
<x-forms.checkbox instantSave
|
||||||
id="application.settings.is_preserve_repository_enabled"
|
id="is_preserve_repository_enabled"
|
||||||
label="Preserve Repository During Deployment"
|
label="Preserve Repository During Deployment"
|
||||||
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
|
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
|
||||||
x-bind:disabled="shouldDisable()" />
|
x-bind:disabled="shouldDisable()" />
|
||||||
|
|
@ -261,12 +261,12 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||||
placeholder="docker compose build"
|
placeholder="docker compose build"
|
||||||
id="application.docker_compose_custom_build_command"
|
id="docker_compose_custom_build_command"
|
||||||
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>"
|
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>"
|
||||||
label="Custom Build Command" />
|
label="Custom Build Command" />
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||||
placeholder="docker compose up -d"
|
placeholder="docker compose up -d"
|
||||||
id="application.docker_compose_custom_start_command"
|
id="docker_compose_custom_start_command"
|
||||||
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
|
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
|
||||||
label="Custom Start Command" />
|
label="Custom Start Command" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -274,36 +274,36 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<x-forms.textarea
|
<x-forms.textarea
|
||||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||||
placeholder="services/api/**" id="application.watch_paths"
|
placeholder="services/api/**" id="watch_paths"
|
||||||
label="Watch Paths" x-bind:disabled="shouldDisable()" />
|
label="Watch Paths" x-bind:disabled="shouldDisable()" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="flex flex-col gap-2 xl:flex-row">
|
<div class="flex flex-col gap-2 xl:flex-row">
|
||||||
<x-forms.input placeholder="/" id="application.base_directory"
|
<x-forms.input placeholder="/" id="base_directory"
|
||||||
label="Base Directory"
|
label="Base Directory"
|
||||||
helper="Directory to use as root. Useful for monorepos."
|
helper="Directory to use as root. Useful for monorepos."
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
|
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
|
||||||
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
|
<x-forms.input placeholder="/Dockerfile" id="dockerfile_location"
|
||||||
label="Dockerfile Location"
|
label="Dockerfile Location"
|
||||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
|
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($application->build_pack === 'dockerfile')
|
@if ($application->build_pack === 'dockerfile')
|
||||||
<x-forms.input id="application.dockerfile_target_build"
|
<x-forms.input id="dockerfile_target_build"
|
||||||
label="Docker Build Stage Target"
|
label="Docker Build Stage Target"
|
||||||
helper="Useful if you have multi-staged dockerfile."
|
helper="Useful if you have multi-staged dockerfile."
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
@if ($application->could_set_build_commands())
|
@if ($application->could_set_build_commands())
|
||||||
@if ($application->settings->is_static)
|
@if ($application->settings->is_static)
|
||||||
<x-forms.input placeholder="/dist" id="application.publish_directory"
|
<x-forms.input placeholder="/dist" id="publish_directory"
|
||||||
label="Publish Directory" required x-bind:disabled="!canUpdate" />
|
label="Publish Directory" required x-bind:disabled="!canUpdate" />
|
||||||
@else
|
@else
|
||||||
<x-forms.input placeholder="/" id="application.publish_directory"
|
<x-forms.input placeholder="/" id="publish_directory"
|
||||||
label="Publish Directory" x-bind:disabled="!canUpdate" />
|
label="Publish Directory" x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -313,21 +313,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div class="pb-4">
|
<div class="pb-4">
|
||||||
<x-forms.textarea
|
<x-forms.textarea
|
||||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||||
placeholder="src/pages/**" id="application.watch_paths"
|
placeholder="src/pages/**" id="watch_paths"
|
||||||
label="Watch Paths" x-bind:disabled="!canUpdate" />
|
label="Watch Paths" x-bind:disabled="!canUpdate" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.input
|
<x-forms.input
|
||||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k --hostname=myapp"
|
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k --hostname=myapp"
|
||||||
id="application.custom_docker_run_options" label="Custom Docker Options"
|
id="custom_docker_run_options" label="Custom Docker Options"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
|
|
||||||
@if ($application->build_pack !== 'dockercompose')
|
@if ($application->build_pack !== 'dockercompose')
|
||||||
<div class="pt-2 w-96">
|
<div class="pt-2 w-96">
|
||||||
<x-forms.checkbox
|
<x-forms.checkbox
|
||||||
helper="Use a build server to build your application. You can configure your build server in the Server settings. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
helper="Use a build server to build your application. You can configure your build server in the Server settings. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||||
instantSave id="application.settings.is_build_server_enabled"
|
instantSave id="is_build_server_enabled"
|
||||||
label="Use a Build Server?" x-bind:disabled="!canUpdate" />
|
label="Use a Build Server?" x-bind:disabled="!canUpdate" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -344,18 +344,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
@if ($application->settings->is_raw_compose_deployment_enabled)
|
@if ($application->settings->is_raw_compose_deployment_enabled)
|
||||||
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
|
<x-forms.textarea rows="10" readonly id="docker_compose_raw"
|
||||||
label="Docker Compose Content (applicationId: {{ $application->id }})"
|
label="Docker Compose Content (applicationId: {{ $application->id }})"
|
||||||
helper="You need to modify the docker compose file in the git repository."
|
helper="You need to modify the docker compose file in the git repository."
|
||||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||||
@else
|
@else
|
||||||
@if ((int) $application->compose_parsing_version >= 3)
|
@if ((int) $application->compose_parsing_version >= 3)
|
||||||
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
|
<x-forms.textarea rows="10" readonly id="docker_compose_raw"
|
||||||
label="Docker Compose Content (raw)"
|
label="Docker Compose Content (raw)"
|
||||||
helper="You need to modify the docker compose file in the git repository."
|
helper="You need to modify the docker compose file in the git repository."
|
||||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||||
@endif
|
@endif
|
||||||
<x-forms.textarea rows="10" readonly id="application.docker_compose"
|
<x-forms.textarea rows="10" readonly id="docker_compose"
|
||||||
label="Docker Compose Content"
|
label="Docker Compose Content"
|
||||||
helper="You need to modify the docker compose file in the git repository."
|
helper="You need to modify the docker compose file in the git repository."
|
||||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||||
|
|
@ -363,45 +363,45 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox label="Escape special characters in labels?"
|
<x-forms.checkbox label="Escape special characters in labels?"
|
||||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||||
id="application.settings.is_container_label_escape_enabled" instantSave
|
id="is_container_label_escape_enabled" instantSave
|
||||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||||
{{-- <x-forms.checkbox label="Readonly labels"
|
{{-- <x-forms.checkbox label="Readonly labels"
|
||||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||||
id="application.settings.is_container_label_readonly_enabled" instantSave></x-forms.checkbox> --}}
|
id="is_container_label_readonly_enabled" instantSave></x-forms.checkbox> --}}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($application->dockerfile)
|
@if ($application->dockerfile)
|
||||||
<x-forms.textarea label="Dockerfile" id="application.dockerfile" monacoEditorLanguage="dockerfile"
|
<x-forms.textarea label="Dockerfile" id="dockerfile" monacoEditorLanguage="dockerfile"
|
||||||
useMonacoEditor rows="6" x-bind:disabled="!canUpdate"> </x-forms.textarea>
|
useMonacoEditor rows="6" x-bind:disabled="!canUpdate"> </x-forms.textarea>
|
||||||
@endif
|
@endif
|
||||||
@if ($application->build_pack !== 'dockercompose')
|
@if ($application->build_pack !== 'dockercompose')
|
||||||
<h3 class="pt-8">Network</h3>
|
<h3 class="pt-8">Network</h3>
|
||||||
<div class="flex flex-col gap-2 xl:flex-row">
|
<div class="flex flex-col gap-2 xl:flex-row">
|
||||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||||
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly
|
<x-forms.input id="ports_exposes" label="Ports Exposes" readonly
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@else
|
@else
|
||||||
@if ($application->settings->is_container_label_readonly_enabled === false)
|
@if ($application->settings->is_container_label_readonly_enabled === false)
|
||||||
<x-forms.input placeholder="3000,3001" id="application.ports_exposes"
|
<x-forms.input placeholder="3000,3001" id="ports_exposes"
|
||||||
label="Ports Exposes" readonly
|
label="Ports Exposes" readonly
|
||||||
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
|
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@else
|
@else
|
||||||
<x-forms.input placeholder="3000,3001" id="application.ports_exposes"
|
<x-forms.input placeholder="3000,3001" id="ports_exposes"
|
||||||
label="Ports Exposes" required
|
label="Ports Exposes" required
|
||||||
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly."
|
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly."
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@if (!$application->destination->server->isSwarm())
|
@if (!$application->destination->server->isSwarm())
|
||||||
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
|
<x-forms.input placeholder="3000:3000" id="ports_mappings" label="Ports Mappings"
|
||||||
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host."
|
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host."
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
@if (!$application->destination->server->isSwarm())
|
@if (!$application->destination->server->isSwarm())
|
||||||
<x-forms.input id="application.custom_network_aliases" label="Network Aliases"
|
<x-forms.input id="custom_network_aliases" label="Network Aliases"
|
||||||
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>api.internal,api.local"
|
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>api.internal,api.local"
|
||||||
wire:model="application.custom_network_aliases" x-bind:disabled="!canUpdate" />
|
wire:model="custom_network_aliases" x-bind:disabled="!canUpdate" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -409,14 +409,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div>
|
<div>
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
||||||
label="Enable" id="application.is_http_basic_auth_enabled"
|
label="Enable" id="is_http_basic_auth_enabled"
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
</div>
|
</div>
|
||||||
@if ($application->is_http_basic_auth_enabled)
|
@if ($application->is_http_basic_auth_enabled)
|
||||||
<div class="flex gap-2 py-2">
|
<div class="flex gap-2 py-2">
|
||||||
<x-forms.input id="application.http_basic_auth_username" label="Username" required
|
<x-forms.input id="http_basic_auth_username" label="Username" required
|
||||||
x-bind:disabled="!canUpdate" />
|
x-bind:disabled="!canUpdate" />
|
||||||
<x-forms.input id="application.http_basic_auth_password" type="password" label="Password"
|
<x-forms.input id="http_basic_auth_password" type="password" label="Password"
|
||||||
required x-bind:disabled="!canUpdate" />
|
required x-bind:disabled="!canUpdate" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -432,11 +432,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox label="Readonly labels"
|
<x-forms.checkbox label="Readonly labels"
|
||||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||||
id="application.settings.is_container_label_readonly_enabled" instantSave
|
id="is_container_label_readonly_enabled" instantSave
|
||||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||||
<x-forms.checkbox label="Escape special characters in labels?"
|
<x-forms.checkbox label="Escape special characters in labels?"
|
||||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||||
id="application.settings.is_container_label_escape_enabled" instantSave
|
id="is_container_label_escape_enabled" instantSave
|
||||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||||
</div>
|
</div>
|
||||||
@can('update', $application)
|
@can('update', $application)
|
||||||
|
|
@ -455,21 +455,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||||
<h3 class="pt-8">Pre/Post Deployment Commands</h3>
|
<h3 class="pt-8">Pre/Post Deployment Commands</h3>
|
||||||
<div class="flex flex-col gap-2 xl:flex-row">
|
<div class="flex flex-col gap-2 xl:flex-row">
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||||
id="application.pre_deployment_command" label="Pre-deployment "
|
id="pre_deployment_command" label="Pre-deployment "
|
||||||
helper="An optional script or command to execute in the existing container before the deployment begins.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
helper="An optional script or command to execute in the existing container before the deployment begins.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
||||||
@if ($application->build_pack === 'dockercompose')
|
@if ($application->build_pack === 'dockercompose')
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()" id="application.pre_deployment_command_container"
|
<x-forms.input x-bind:disabled="shouldDisable()" id="pre_deployment_command_container"
|
||||||
label="Container Name"
|
label="Container Name"
|
||||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 xl:flex-row">
|
<div class="flex flex-col gap-2 xl:flex-row">
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||||
id="application.post_deployment_command" label="Post-deployment "
|
id="post_deployment_command" label="Post-deployment "
|
||||||
helper="An optional script or command to execute in the newly built container after the deployment completes.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
helper="An optional script or command to execute in the newly built container after the deployment completes.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
||||||
@if ($application->build_pack === 'dockercompose')
|
@if ($application->build_pack === 'dockercompose')
|
||||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||||
id="application.post_deployment_command_container" label="Container Name"
|
id="post_deployment_command_container" label="Container Name"
|
||||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<form wire:submit="save" class="flex items-end gap-2">
|
<form wire:submit="save" class="flex items-end gap-2">
|
||||||
<x-forms.input helper="One domain per preview." label="Domains for {{ str($serviceName)->headline() }}"
|
<x-forms.input helper="One domain per preview." label="Domains for {{ str($serviceName)->headline() }}" id="domain"
|
||||||
id="service.domain" canGate="update" :canResource="$preview->application"></x-forms.input>
|
canGate="update" :canResource="$preview->application"></x-forms.input>
|
||||||
<x-forms.button type="submit">Save</x-forms.button>
|
<x-forms.button type="submit">Save</x-forms.button>
|
||||||
<x-forms.button wire:click="generate">Generate
|
<x-forms.button wire:click="generate">Generate
|
||||||
Domain</x-forms.button>
|
Domain</x-forms.button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -112,7 +112,7 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
|
||||||
<form wire:submit="save_preview('{{ $preview->id }}')"
|
<form wire:submit="save_preview('{{ $preview->id }}')"
|
||||||
class="flex items-end gap-2 pt-4">
|
class="flex items-end gap-2 pt-4">
|
||||||
<x-forms.input label="Domain" helper="One domain per preview."
|
<x-forms.input label="Domain" helper="One domain per preview."
|
||||||
id="application.previews.{{ $previewName }}.fqdn" canGate="update" :canResource="$application"></x-forms.input>
|
id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
|
||||||
@can('update', $application)
|
@can('update', $application)
|
||||||
<x-forms.button type="submit">Save</x-forms.button>
|
<x-forms.button type="submit">Save</x-forms.button>
|
||||||
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
|
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
|
||||||
|
|
@ -130,7 +130,7 @@ class="flex items-end gap-2 pt-4">
|
||||||
@else
|
@else
|
||||||
<form wire:submit="save_preview('{{ $preview->id }}')" class="flex items-end gap-2 pt-4">
|
<form wire:submit="save_preview('{{ $preview->id }}')" class="flex items-end gap-2 pt-4">
|
||||||
<x-forms.input label="Domain" helper="One domain per preview."
|
<x-forms.input label="Domain" helper="One domain per preview."
|
||||||
id="application.previews.{{ $previewName }}.fqdn" canGate="update" :canResource="$application"></x-forms.input>
|
id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
|
||||||
@can('update', $application)
|
@can('update', $application)
|
||||||
<x-forms.button type="submit">Save</x-forms.button>
|
<x-forms.button type="submit">Save</x-forms.button>
|
||||||
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
|
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$database" label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
|
<x-forms.input canGate="update" :canResource="$database" label="Name" id="humanName" placeholder="Name"></x-forms.input>
|
||||||
<x-forms.input canGate="update" :canResource="$database" label="Description" id="database.description"></x-forms.input>
|
<x-forms.input canGate="update" :canResource="$database" label="Description" id="description"></x-forms.input>
|
||||||
<x-forms.input canGate="update" :canResource="$database" required
|
<x-forms.input canGate="update" :canResource="$database" required
|
||||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||||
label="Image" id="database.image"></x-forms.input>
|
label="Image" id="image"></x-forms.input>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end gap-2">
|
<div class="flex items-end gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$database" placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
|
<x-forms.input canGate="update" :canResource="$database" placeholder="5432" disabled="{{ $database->is_public }}" id="publicPort"
|
||||||
label="Public Port" />
|
label="Public Port" />
|
||||||
<x-forms.checkbox canGate="update" :canResource="$database" instantSave id="database.is_public" label="Make it publicly available" />
|
<x-forms.checkbox canGate="update" :canResource="$database" instantSave id="isPublic" label="Make it publicly available" />
|
||||||
</div>
|
</div>
|
||||||
@if ($db_url_public)
|
@if ($db_url_public)
|
||||||
<x-forms.input label="Database IP:PORT (public)"
|
<x-forms.input label="Database IP:PORT (public)"
|
||||||
|
|
@ -44,9 +44,9 @@
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox canGate="update" :canResource="$database" instantSave="instantSaveExclude" label="Exclude from service status"
|
<x-forms.checkbox canGate="update" :canResource="$database" instantSave="instantSaveExclude" label="Exclude from service status"
|
||||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||||
id="database.exclude_from_status"></x-forms.checkbox>
|
id="excludeFromStatus"></x-forms.checkbox>
|
||||||
<x-forms.checkbox canGate="update" :canResource="$database" helper="Drain logs to your configured log drain endpoint in your Server settings."
|
<x-forms.checkbox canGate="update" :canResource="$database" helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||||
instantSave="instantSaveLogDrain" id="database.is_log_drain_enabled" label="Drain Logs" />
|
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,24 @@
|
||||||
|
|
||||||
<div x-cloak x-show="raw" class="font-mono">
|
<div x-cloak x-show="raw" class="font-mono">
|
||||||
<div x-cloak x-show="showNormalTextarea">
|
<div x-cloak x-show="showNormalTextarea">
|
||||||
<x-forms.textarea rows="20" id="service.docker_compose_raw">
|
<x-forms.textarea rows="20" id="dockerComposeRaw">
|
||||||
</x-forms.textarea>
|
</x-forms.textarea>
|
||||||
</div>
|
</div>
|
||||||
<div x-cloak x-show="!showNormalTextarea">
|
<div x-cloak x-show="!showNormalTextarea">
|
||||||
<x-forms.textarea allowTab useMonacoEditor monacoEditorLanguage="yaml" rows="20"
|
<x-forms.textarea allowTab useMonacoEditor monacoEditorLanguage="yaml" rows="20"
|
||||||
id="service.docker_compose_raw">
|
id="dockerComposeRaw">
|
||||||
</x-forms.textarea>
|
</x-forms.textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div x-cloak x-show="raw === false" class="font-mono">
|
<div x-cloak x-show="raw === false" class="font-mono">
|
||||||
<x-forms.textarea rows="20" readonly id="service.docker_compose">
|
<x-forms.textarea rows="20" readonly id="dockerCompose">
|
||||||
</x-forms.textarea>
|
</x-forms.textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-2 flex gap-2">
|
<div class="pt-2 flex gap-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<x-forms.checkbox label="Escape special characters in labels?"
|
<x-forms.checkbox label="Escape special characters in labels?"
|
||||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||||
id="service.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
|
id="isContainerLabelEscapeEnabled" instantSave></x-forms.checkbox>
|
||||||
<x-forms.checkbox label="Show Normal Textarea" x-model="showNormalTextarea"></x-forms.checkbox>
|
<x-forms.checkbox label="Show Normal Textarea" x-model="showNormalTextarea"></x-forms.checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="pb-2">Note: If a service has a defined port, do not delete it. <br>If you want to use your custom
|
<div class="pb-2">Note: If a service has a defined port, do not delete it. <br>If you want to use your custom
|
||||||
domain, you can add it with a port.</div>
|
domain, you can add it with a port.</div>
|
||||||
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
|
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
|
||||||
id="application.fqdn"
|
id="fqdn"
|
||||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,12 @@
|
||||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox instantSave label="Is this based on the Git repository?"
|
<x-forms.checkbox instantSave label="Is this based on the Git repository?"
|
||||||
id="fileStorage.is_based_on_git"></x-forms.checkbox>
|
id="isBasedOnGit"></x-forms.checkbox>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.textarea
|
<x-forms.textarea
|
||||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||||
rows="20" id="fileStorage.content"
|
rows="20" id="content"
|
||||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
||||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
||||||
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
|
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
|
||||||
|
|
@ -74,12 +74,12 @@
|
||||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||||
id="fileStorage.is_based_on_git"></x-forms.checkbox>
|
id="isBasedOnGit"></x-forms.checkbox>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.textarea
|
<x-forms.textarea
|
||||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||||
rows="20" id="fileStorage.content" disabled></x-forms.textarea>
|
rows="20" id="content" disabled></x-forms.textarea>
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
|
|
@ -88,12 +88,12 @@
|
||||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||||
id="fileStorage.is_based_on_git"></x-forms.checkbox>
|
id="isBasedOnGit"></x-forms.checkbox>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.textarea
|
<x-forms.textarea
|
||||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||||
rows="20" id="fileStorage.content" disabled></x-forms.textarea>
|
rows="20" id="content" disabled></x-forms.textarea>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -23,48 +23,48 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$application" label="Name" id="application.human_name"
|
<x-forms.input canGate="update" :canResource="$application" label="Name" id="humanName"
|
||||||
placeholder="Human readable name"></x-forms.input>
|
placeholder="Human readable name"></x-forms.input>
|
||||||
<x-forms.input canGate="update" :canResource="$application" label="Description"
|
<x-forms.input canGate="update" :canResource="$application" label="Description"
|
||||||
id="application.description"></x-forms.input>
|
id="description"></x-forms.input>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
|
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||||
@if ($application->required_fqdn)
|
@if ($application->required_fqdn)
|
||||||
<x-forms.input canGate="update" :canResource="$application" required placeholder="https://app.coolify.io"
|
<x-forms.input canGate="update" :canResource="$application" required placeholder="https://app.coolify.io"
|
||||||
label="Domains" id="application.fqdn"
|
label="Domains" id="fqdn"
|
||||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||||
@else
|
@else
|
||||||
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io"
|
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io"
|
||||||
label="Domains" id="application.fqdn"
|
label="Domains" id="fqdn"
|
||||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
<x-forms.input canGate="update" :canResource="$application"
|
<x-forms.input canGate="update" :canResource="$application"
|
||||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||||
label="Image" id="application.image"></x-forms.input>
|
label="Image" id="image"></x-forms.input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="py-2 pt-4">Advanced</h3>
|
<h3 class="py-2 pt-4">Advanced</h3>
|
||||||
<div class="w-96 flex flex-col gap-1">
|
<div class="w-96 flex flex-col gap-1">
|
||||||
@if (str($application->image)->contains('pocketbase'))
|
@if (str($application->image)->contains('pocketbase'))
|
||||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="application.is_gzip_enabled"
|
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="isGzipEnabled"
|
||||||
label="Enable Gzip Compression"
|
label="Enable Gzip Compression"
|
||||||
helper="Pocketbase does not need gzip compression, otherwise SSE will not work." disabled />
|
helper="Pocketbase does not need gzip compression, otherwise SSE will not work." disabled />
|
||||||
@else
|
@else
|
||||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="application.is_gzip_enabled"
|
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="isGzipEnabled"
|
||||||
label="Enable Gzip Compression"
|
label="Enable Gzip Compression"
|
||||||
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
|
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
|
||||||
@endif
|
@endif
|
||||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="application.is_stripprefix_enabled"
|
<x-forms.checkbox canGate="update" :canResource="$application" instantSave id="isStripprefixEnabled"
|
||||||
label="Strip Prefixes"
|
label="Strip Prefixes"
|
||||||
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
|
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
|
||||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave label="Exclude from service status"
|
<x-forms.checkbox canGate="update" :canResource="$application" instantSave label="Exclude from service status"
|
||||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||||
id="application.exclude_from_status"></x-forms.checkbox>
|
id="excludeFromStatus"></x-forms.checkbox>
|
||||||
<x-forms.checkbox canGate="update" :canResource="$application"
|
<x-forms.checkbox canGate="update" :canResource="$application"
|
||||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||||
instantSave="instantSaveAdvanced" id="application.is_log_drain_enabled" label="Drain Logs" />
|
instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
<div>Configuration</div>
|
<div>Configuration</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$service" id="service.name" required label="Service Name" placeholder="My super WordPress site" />
|
<x-forms.input canGate="update" :canResource="$service" id="name" required label="Service Name" placeholder="My super WordPress site" />
|
||||||
<x-forms.input canGate="update" :canResource="$service" id="service.description" label="Description" />
|
<x-forms.input canGate="update" :canResource="$service" id="description" label="Description" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-96">
|
<div class="w-96">
|
||||||
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="service.connect_to_docker_network" label="Connect To Predefined Network"
|
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="connectToDockerNetwork" label="Connect To Predefined Network"
|
||||||
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
|
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
|
||||||
</div>
|
</div>
|
||||||
@if ($fields->count() > 0)
|
@if ($fields->count() > 0)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2>Healthchecks</h2>
|
<h2>Healthchecks</h2>
|
||||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
|
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
|
||||||
@if (!$resource->health_check_enabled)
|
@if (!$healthCheckEnabled)
|
||||||
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||||
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this resource.']"
|
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this resource.']"
|
||||||
warningMessage="If the health check fails, your application will become inaccessible. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
|
warningMessage="If the health check fails, your application will become inaccessible. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
|
||||||
|
|
@ -15,37 +15,37 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 pb-4">Define how your resource's health should be checked.</div>
|
<div class="mt-1 pb-4">Define how your resource's health should be checked.</div>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
@if ($resource->custom_healthcheck_found)
|
@if ($customHealthcheckFound)
|
||||||
<x-callout type="warning" title="Caution">
|
<x-callout type="warning" title="Caution">
|
||||||
<p>A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.</p>
|
<p>A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.</p>
|
||||||
</x-callout>
|
</x-callout>
|
||||||
@endif
|
@endif
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.select canGate="update" :canResource="$resource" id="resource.health_check_method" label="Method" required>
|
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckMethod" label="Method" required>
|
||||||
<option value="GET">GET</option>
|
<option value="GET">GET</option>
|
||||||
<option value="POST">POST</option>
|
<option value="POST">POST</option>
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
<x-forms.select canGate="update" :canResource="$resource" id="resource.health_check_scheme" label="Scheme" required>
|
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckScheme" label="Scheme" required>
|
||||||
<option value="http">http</option>
|
<option value="http">http</option>
|
||||||
<option value="https">https</option>
|
<option value="https">https</option>
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
<x-forms.input canGate="update" :canResource="$resource" id="resource.health_check_host" placeholder="localhost" label="Host" required />
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckHost" placeholder="localhost" label="Host" required />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="resource.health_check_port"
|
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckPort"
|
||||||
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" id="resource.health_check_path" placeholder="/health" label="Path" required />
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckPath" placeholder="/health" label="Path" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="resource.health_check_return_code" placeholder="200" label="Return Code"
|
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckReturnCode" placeholder="200" label="Return Code"
|
||||||
required />
|
required />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" id="resource.health_check_response_text" placeholder="OK" label="Response Text" />
|
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckResponseText" placeholder="OK" label="Response Text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="resource.health_check_interval" placeholder="30"
|
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="healthCheckInterval" placeholder="30"
|
||||||
label="Interval (s)" required />
|
label="Interval (s)" required />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="resource.health_check_timeout" placeholder="30" label="Timeout (s)"
|
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckTimeout" placeholder="30" label="Timeout (s)"
|
||||||
required />
|
required />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="resource.health_check_retries" placeholder="3" label="Retries" required />
|
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckRetries" placeholder="3" label="Retries" required />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" min=1 type="number" id="resource.health_check_start_period" placeholder="30"
|
<x-forms.input canGate="update" :canResource="$resource" min=1 type="number" id="healthCheckStartPeriod" placeholder="30"
|
||||||
label="Start Period (s)" required />
|
label="Start Period (s)" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,32 +9,32 @@
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="1.5"
|
<x-forms.input canGate="update" :canResource="$resource" placeholder="1.5"
|
||||||
helper="0 means use all CPUs. Floating point number, like 0.002 or 1.5. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
helper="0 means use all CPUs. Floating point number, like 0.002 or 1.5. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
||||||
label="Number of CPUs" id="resource.limits_cpus" />
|
label="Number of CPUs" id="limitsCpus" />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="0-2"
|
<x-forms.input canGate="update" :canResource="$resource" placeholder="0-2"
|
||||||
helper="Empty means, use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
helper="Empty means, use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
||||||
label="CPU sets to use" id="resource.limits_cpuset" />
|
label="CPU sets to use" id="limitsCpuset" />
|
||||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="1024"
|
<x-forms.input canGate="update" :canResource="$resource" placeholder="1024"
|
||||||
helper="More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
helper="More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
||||||
label="CPU Weight" id="resource.limits_cpu_shares" />
|
label="CPU Weight" id="limitsCpuShares" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="pt-4">Limit Memory</h3>
|
<h3 class="pt-4">Limit Memory</h3>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource"
|
<x-forms.input canGate="update" :canResource="$resource"
|
||||||
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
|
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
|
||||||
label="Soft Memory Limit" id="resource.limits_memory_reservation" />
|
label="Soft Memory Limit" id="limitsMemoryReservation" />
|
||||||
<x-forms.input canGate="update" :canResource="$resource"
|
<x-forms.input canGate="update" :canResource="$resource"
|
||||||
helper="0-100.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
|
helper="0-100.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
|
||||||
type="number" min="0" max="100" label="Swappiness"
|
type="number" min="0" max="100" label="Swappiness"
|
||||||
id="resource.limits_memory_swappiness" />
|
id="limitsMemorySwappiness" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource"
|
<x-forms.input canGate="update" :canResource="$resource"
|
||||||
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
|
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
|
||||||
label="Maximum Memory Limit" id="resource.limits_memory" />
|
label="Maximum Memory Limit" id="limitsMemory" />
|
||||||
<x-forms.input canGate="update" :canResource="$resource"
|
<x-forms.input canGate="update" :canResource="$resource"
|
||||||
helper="Examples:69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
|
helper="Examples:69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
|
||||||
label="Maximum Swap Limit" id="resource.limits_memory_swap" />
|
label="Maximum Swap Limit" id="limitsMemorySwap" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -9,47 +9,47 @@
|
||||||
@if (
|
@if (
|
||||||
$storage->resource_type === 'App\Models\ServiceApplication' ||
|
$storage->resource_type === 'App\Models\ServiceApplication' ||
|
||||||
$storage->resource_type === 'App\Models\ServiceDatabase')
|
$storage->resource_type === 'App\Models\ServiceDatabase')
|
||||||
<x-forms.input id="storage.name" label="Volume Name" required readonly
|
<x-forms.input id="name" label="Volume Name" required readonly
|
||||||
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing." />
|
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing." />
|
||||||
@else
|
@else
|
||||||
<x-forms.input id="storage.name" label="Volume Name" required readonly
|
<x-forms.input id="name" label="Volume Name" required readonly
|
||||||
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing." />
|
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing." />
|
||||||
@endif
|
@endif
|
||||||
@if ($isService || $startedAt)
|
@if ($isService || $startedAt)
|
||||||
<x-forms.input id="storage.host_path" readonly helper="Directory on the host system."
|
<x-forms.input id="hostPath" readonly helper="Directory on the host system."
|
||||||
label="Source Path"
|
label="Source Path"
|
||||||
helper="Warning: Changing the source path after the initial start could cause problems. Only use it when you know what are you doing." />
|
helper="Warning: Changing the source path after the initial start could cause problems. Only use it when you know what are you doing." />
|
||||||
<x-forms.input id="storage.mount_path" label="Destination Path"
|
<x-forms.input id="mountPath" label="Destination Path"
|
||||||
helper="Directory inside the container." required readonly />
|
helper="Directory inside the container." required readonly />
|
||||||
@else
|
@else
|
||||||
<x-forms.input id="storage.host_path" readonly helper="Directory on the host system."
|
<x-forms.input id="hostPath" readonly helper="Directory on the host system."
|
||||||
label="Source Path"
|
label="Source Path"
|
||||||
helper="Warning: Changing the source path after the initial start could cause problems. Only use it when you know what are you doing." />
|
helper="Warning: Changing the source path after the initial start could cause problems. Only use it when you know what are you doing." />
|
||||||
<x-forms.input id="storage.mount_path" label="Destination Path"
|
<x-forms.input id="mountPath" label="Destination Path"
|
||||||
helper="Directory inside the container." required readonly />
|
helper="Directory inside the container." required readonly />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="flex gap-2 items-end w-full">
|
<div class="flex gap-2 items-end w-full">
|
||||||
<x-forms.input id="storage.name" required readonly />
|
<x-forms.input id="name" required readonly />
|
||||||
<x-forms.input id="storage.host_path" readonly />
|
<x-forms.input id="hostPath" readonly />
|
||||||
<x-forms.input id="storage.mount_path" required readonly />
|
<x-forms.input id="mountPath" required readonly />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
@can('update', $resource)
|
@can('update', $resource)
|
||||||
@if ($isFirst)
|
@if ($isFirst)
|
||||||
<div class="flex gap-2 items-end w-full">
|
<div class="flex gap-2 items-end w-full">
|
||||||
<x-forms.input id="storage.name" label="Volume Name" required />
|
<x-forms.input id="name" label="Volume Name" required />
|
||||||
<x-forms.input id="storage.host_path" helper="Directory on the host system." label="Source Path" />
|
<x-forms.input id="hostPath" helper="Directory on the host system." label="Source Path" />
|
||||||
<x-forms.input id="storage.mount_path" label="Destination Path"
|
<x-forms.input id="mountPath" label="Destination Path"
|
||||||
helper="Directory inside the container." required />
|
helper="Directory inside the container." required />
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="flex gap-2 items-end w-full">
|
<div class="flex gap-2 items-end w-full">
|
||||||
<x-forms.input id="storage.name" required />
|
<x-forms.input id="name" required />
|
||||||
<x-forms.input id="storage.host_path" />
|
<x-forms.input id="hostPath" />
|
||||||
<x-forms.input id="storage.mount_path" required />
|
<x-forms.input id="mountPath" required />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
|
@ -67,17 +67,17 @@
|
||||||
@else
|
@else
|
||||||
@if ($isFirst)
|
@if ($isFirst)
|
||||||
<div class="flex gap-2 items-end w-full">
|
<div class="flex gap-2 items-end w-full">
|
||||||
<x-forms.input id="storage.name" label="Volume Name" required disabled />
|
<x-forms.input id="name" label="Volume Name" required disabled />
|
||||||
<x-forms.input id="storage.host_path" helper="Directory on the host system." label="Source Path"
|
<x-forms.input id="hostPath" helper="Directory on the host system." label="Source Path"
|
||||||
disabled />
|
disabled />
|
||||||
<x-forms.input id="storage.mount_path" label="Destination Path"
|
<x-forms.input id="mountPath" label="Destination Path"
|
||||||
helper="Directory inside the container." required disabled />
|
helper="Directory inside the container." required disabled />
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="flex gap-2 items-end w-full">
|
<div class="flex gap-2 items-end w-full">
|
||||||
<x-forms.input id="storage.name" required disabled />
|
<x-forms.input id="name" required disabled />
|
||||||
<x-forms.input id="storage.host_path" disabled />
|
<x-forms.input id="hostPath" disabled />
|
||||||
<x-forms.input id="storage.mount_path" required disabled />
|
<x-forms.input id="mountPath" required disabled />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endcan
|
@endcan
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$private_key" id="private_key.name" label="Name" required />
|
<x-forms.input canGate="update" :canResource="$private_key" id="name" label="Name" required />
|
||||||
<x-forms.input canGate="update" :canResource="$private_key" id="private_key.description" label="Description" />
|
<x-forms.input canGate="update" :canResource="$private_key" id="description" label="Description" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-end gap-2 py-2 ">
|
<div class="flex items-end gap-2 py-2 ">
|
||||||
|
|
@ -46,17 +46,17 @@
|
||||||
Hide
|
Hide
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (data_get($private_key, 'is_git_related'))
|
@if ($isGitRelated)
|
||||||
<div class="w-48">
|
<div class="w-48">
|
||||||
<x-forms.checkbox id="private_key.is_git_related" disabled label="Is used by a Git App?" />
|
<x-forms.checkbox id="isGitRelated" disabled label="Is used by a Git App?" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div x-cloak x-show="!showPrivateKey">
|
<div x-cloak x-show="!showPrivateKey">
|
||||||
<x-forms.input canGate="update" :canResource="$private_key" allowToPeak="false" type="password" rows="10" id="private_key.private_key"
|
<x-forms.input canGate="update" :canResource="$private_key" allowToPeak="false" type="password" rows="10" id="privateKeyValue"
|
||||||
required disabled />
|
required disabled />
|
||||||
</div>
|
</div>
|
||||||
<div x-cloak x-show="showPrivateKey">
|
<div x-cloak x-show="showPrivateKey">
|
||||||
<x-forms.textarea canGate="update" :canResource="$private_key" rows="10" id="private_key.private_key" required />
|
<x-forms.textarea canGate="update" :canResource="$private_key" rows="10" id="privateKeyValue" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
<div class="pb-6 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="generateExactLabels"
|
||||||
label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave />
|
label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave />
|
||||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave="instantSaveRedirect"
|
<x-forms.checkbox canGate="update" :canResource="$server" instantSave="instantSaveRedirect"
|
||||||
id="redirectEnabled" label="Override default request handler"
|
id="redirectEnabled" label="Override default request handler"
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="flex items-end gap-2 w-full">
|
<div class="flex items-end gap-2 w-full">
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.name" label="App Name" />
|
<x-forms.input canGate="update" :canResource="$github_app" id="name" label="App Name" />
|
||||||
<x-forms.button canGate="update" :canResource="$github_app" wire:click.prevent="updateGithubAppName">
|
<x-forms.button canGate="update" :canResource="$github_app" wire:click.prevent="updateGithubAppName">
|
||||||
Sync Name
|
Sync Name
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
|
|
@ -64,41 +64,41 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.organization" label="Organization"
|
<x-forms.input canGate="update" :canResource="$github_app" id="organization" label="Organization"
|
||||||
placeholder="If empty, personal user will be used" />
|
placeholder="If empty, personal user will be used" />
|
||||||
@if (!isCloud())
|
@if (!isCloud())
|
||||||
<div class="w-48">
|
<div class="w-48">
|
||||||
<x-forms.checkbox canGate="update" :canResource="$github_app" label="System Wide?"
|
<x-forms.checkbox canGate="update" :canResource="$github_app" label="System Wide?"
|
||||||
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
|
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
|
||||||
instantSave id="github_app.is_system_wide" />
|
instantSave id="isSystemWide" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.html_url" label="HTML Url" />
|
<x-forms.input canGate="update" :canResource="$github_app" id="htmlUrl" label="HTML Url" />
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.api_url" label="API Url" />
|
<x-forms.input canGate="update" :canResource="$github_app" id="apiUrl" label="API Url" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.custom_user" label="User"
|
<x-forms.input canGate="update" :canResource="$github_app" id="customUser" label="User"
|
||||||
required />
|
required />
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="github_app.custom_port"
|
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="customPort"
|
||||||
label="Port" required />
|
label="Port" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="github_app.app_id"
|
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="appId"
|
||||||
label="App Id" required />
|
label="App Id" required />
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" type="number"
|
<x-forms.input canGate="update" :canResource="$github_app" type="number"
|
||||||
id="github_app.installation_id" label="Installation Id" required />
|
id="installationId" label="Installation Id" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.client_id" label="Client Id"
|
<x-forms.input canGate="update" :canResource="$github_app" id="clientId" label="Client Id"
|
||||||
type="password" required />
|
type="password" required />
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.client_secret"
|
<x-forms.input canGate="update" :canResource="$github_app" id="clientSecret"
|
||||||
label="Client Secret" type="password" required />
|
label="Client Secret" type="password" required />
|
||||||
<x-forms.input canGate="update" :canResource="$github_app" id="github_app.webhook_secret"
|
<x-forms.input canGate="update" :canResource="$github_app" id="webhookSecret"
|
||||||
label="Webhook Secret" type="password" required />
|
label="Webhook Secret" type="password" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.select canGate="update" :canResource="$github_app" id="github_app.private_key_id"
|
<x-forms.select canGate="update" :canResource="$github_app" id="privateKeyId"
|
||||||
label="Private Key" required>
|
label="Private Key" required>
|
||||||
@if (blank($github_app->private_key_id))
|
@if (blank($github_app->private_key_id))
|
||||||
<option value="0" selected>Select a private key</option>
|
<option value="0" selected>Select a private key</option>
|
||||||
|
|
@ -121,14 +121,14 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input id="github_app.contents" helper="read - mandatory." label="Content" readonly
|
<x-forms.input id="contents" helper="read - mandatory." label="Content" readonly
|
||||||
placeholder="N/A" />
|
placeholder="N/A" />
|
||||||
<x-forms.input id="github_app.metadata" helper="read - mandatory." label="Metadata" readonly
|
<x-forms.input id="metadata" helper="read - mandatory." label="Metadata" readonly
|
||||||
placeholder="N/A" />
|
placeholder="N/A" />
|
||||||
{{-- <x-forms.input id="github_app.administration"
|
{{-- <x-forms.input id="administration"
|
||||||
helper="read:write access needed to setup servers as GitHub Runner." label="Administration"
|
helper="read:write access needed to setup servers as GitHub Runner." label="Administration"
|
||||||
readonly placeholder="N/A" /> --}}
|
readonly placeholder="N/A" /> --}}
|
||||||
<x-forms.input id="github_app.pull_requests"
|
<x-forms.input id="pullRequests"
|
||||||
helper="write access needed to use deployment status update in previews."
|
helper="write access needed to use deployment status update in previews."
|
||||||
label="Pull Request" readonly placeholder="N/A" />
|
label="Pull Request" readonly placeholder="N/A" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="subtitle">{{ $storage->name }}</div>
|
<div class="subtitle">{{ $storage->name }}</div>
|
||||||
<div class="flex items-center gap-2 pb-4">
|
<div class="flex items-center gap-2 pb-4">
|
||||||
<div>Current Status:</div>
|
<div>Current Status:</div>
|
||||||
@if ($storage->is_usable)
|
@if ($isUsable)
|
||||||
<span
|
<span
|
||||||
class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:text-green-100 dark:bg-green-800">
|
class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:text-green-100 dark:bg-green-800">
|
||||||
Usable
|
Usable
|
||||||
|
|
@ -32,19 +32,19 @@ class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded dark:text
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$storage" label="Name" id="storage.name" />
|
<x-forms.input canGate="update" :canResource="$storage" label="Name" id="name" />
|
||||||
<x-forms.input canGate="update" :canResource="$storage" label="Description" id="storage.description" />
|
<x-forms.input canGate="update" :canResource="$storage" label="Description" id="description" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$storage" required label="Endpoint" id="storage.endpoint" />
|
<x-forms.input canGate="update" :canResource="$storage" required label="Endpoint" id="endpoint" />
|
||||||
<x-forms.input canGate="update" :canResource="$storage" required label="Bucket" id="storage.bucket" />
|
<x-forms.input canGate="update" :canResource="$storage" required label="Bucket" id="bucket" />
|
||||||
<x-forms.input canGate="update" :canResource="$storage" required label="Region" id="storage.region" />
|
<x-forms.input canGate="update" :canResource="$storage" required label="Region" id="region" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Access Key"
|
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Access Key"
|
||||||
id="storage.key" />
|
id="key" />
|
||||||
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Secret Key"
|
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Secret Key"
|
||||||
id="storage.secret" />
|
id="secret" />
|
||||||
</div>
|
</div>
|
||||||
@can('validateConnection', $storage)
|
@can('validateConnection', $storage)
|
||||||
<x-forms.button class="mt-4" isHighlighted wire:click="testConnection">
|
<x-forms.button class="mt-4" isHighlighted wire:click="testConnection">
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-end gap-2 pb-6">
|
<div class="flex items-end gap-2 pb-6">
|
||||||
<x-forms.input id="team.name" label="Name" required canGate="update" :canResource="$team" />
|
<x-forms.input id="name" label="Name" required canGate="update" :canResource="$team" />
|
||||||
<x-forms.input id="team.description" label="Description" canGate="update" :canResource="$team" />
|
<x-forms.input id="description" label="Description" canGate="update" :canResource="$team" />
|
||||||
@can('update', $team)
|
@can('update', $team)
|
||||||
<x-forms.button type="submit">
|
<x-forms.button type="submit">
|
||||||
Save
|
Save
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue