Merge pull request #7100 from coollabsio/fix-static-builds
Enhance Livewire component data synchronization guidelines
This commit is contained in:
commit
bf0d34c3ef
10 changed files with 1045 additions and 392 deletions
|
|
@ -267,18 +267,365 @@ For complete documentation, see **[form-components.mdc](mdc:.cursor/rules/form-c
|
|||
|
||||
## Form Handling Patterns
|
||||
|
||||
### Livewire Component Data Synchronization Pattern
|
||||
|
||||
**IMPORTANT**: All Livewire components must use the **manual `syncData()` pattern** for synchronizing component properties with Eloquent models.
|
||||
|
||||
#### Property Naming Convention
|
||||
- **Component properties**: Use camelCase (e.g., `$gitRepository`, `$isStatic`)
|
||||
- **Database columns**: Use snake_case (e.g., `git_repository`, `is_static`)
|
||||
- **View bindings**: Use camelCase matching component properties (e.g., `id="gitRepository"`)
|
||||
|
||||
#### The syncData() Method Pattern
|
||||
|
||||
```php
|
||||
use Livewire\Attributes\Validate;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
class MyComponent extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Application $application;
|
||||
|
||||
// Properties with validation attributes
|
||||
#[Validate(['required'])]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isStatic = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncData(); // Load from model
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync TO model (camelCase → snake_case)
|
||||
$this->application->name = $this->name;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->is_static = $this->isStatic;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync FROM model (snake_case → camelCase)
|
||||
$this->name = $this->application->name;
|
||||
$this->description = $this->application->description;
|
||||
$this->isStatic = $this->application->is_static;
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(toModel: true); // Save to model
|
||||
$this->dispatch('success', 'Saved successfully.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Validation with #[Validate] Attributes
|
||||
|
||||
All component properties should have `#[Validate]` attributes:
|
||||
|
||||
```php
|
||||
// Boolean properties
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isEnabled = false;
|
||||
|
||||
// Required strings
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $name;
|
||||
|
||||
// Nullable strings
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
// With constraints
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $timeout;
|
||||
```
|
||||
|
||||
#### Benefits of syncData() Pattern
|
||||
|
||||
- **Explicit Control**: Clear visibility of what's being synchronized
|
||||
- **Type Safety**: #[Validate] attributes provide compile-time validation info
|
||||
- **Easy Debugging**: Single method to check for data flow issues
|
||||
- **Maintainability**: All sync logic in one place
|
||||
- **Flexibility**: Can add custom logic (encoding, transformations, etc.)
|
||||
|
||||
#### Creating New Form Components with syncData()
|
||||
|
||||
#### Step-by-Step Component Creation Guide
|
||||
|
||||
**Step 1: Define properties in camelCase with #[Validate] attributes**
|
||||
```php
|
||||
use Livewire\Attributes\Validate;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class MyFormComponent extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
// The model we're syncing with
|
||||
public Application $application;
|
||||
|
||||
// Component properties in camelCase with validation
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $gitRepository = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $installCommand = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isStatic = false;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Implement syncData() method**
|
||||
```php
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync TO model (component camelCase → database snake_case)
|
||||
$this->application->name = $this->name;
|
||||
$this->application->git_repository = $this->gitRepository;
|
||||
$this->application->install_command = $this->installCommand;
|
||||
$this->application->is_static = $this->isStatic;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync FROM model (database snake_case → component camelCase)
|
||||
$this->name = $this->application->name;
|
||||
$this->gitRepository = $this->application->git_repository;
|
||||
$this->installCommand = $this->application->install_command;
|
||||
$this->isStatic = $this->application->is_static;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Implement mount() to load initial data**
|
||||
```php
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncData(); // Load data from model to component properties
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Implement action methods with authorization**
|
||||
```php
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(toModel: true); // Save component properties to model
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(toModel: true); // Save component properties to model
|
||||
$this->dispatch('success', 'Changes saved successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Create Blade view with camelCase bindings**
|
||||
```blade
|
||||
<div>
|
||||
<form wire:submit="submit">
|
||||
<x-forms.input
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="name"
|
||||
label="Name"
|
||||
required />
|
||||
|
||||
<x-forms.input
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="gitRepository"
|
||||
label="Git Repository" />
|
||||
|
||||
<x-forms.input
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="installCommand"
|
||||
label="Install Command" />
|
||||
|
||||
<x-forms.checkbox
|
||||
instantSave
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="isStatic"
|
||||
label="Static Site" />
|
||||
|
||||
<x-forms.button
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
type="submit">
|
||||
Save Changes
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Use `wire:model="camelCase"` and `id="camelCase"` in Blade views
|
||||
- Component properties are camelCase, database columns are snake_case
|
||||
- Always include authorization checks (`authorize()`, `canGate`, `canResource`)
|
||||
- Use `instantSave` for checkboxes that save immediately without form submission
|
||||
|
||||
#### Special Patterns
|
||||
|
||||
**Pattern 1: Related Models (e.g., Application → Settings)**
|
||||
```php
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync main model
|
||||
$this->application->name = $this->name;
|
||||
$this->application->save();
|
||||
|
||||
// Sync related model
|
||||
$this->application->settings->is_static = $this->isStatic;
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
// From main model
|
||||
$this->name = $this->application->name;
|
||||
|
||||
// From related model
|
||||
$this->isStatic = $this->application->settings->is_static;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Custom Encoding/Decoding**
|
||||
```php
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Encode before saving
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Decode when loading
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Error Rollback**
|
||||
```php
|
||||
public function submit()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$original = $this->model->getOriginal();
|
||||
|
||||
try {
|
||||
$this->syncData(toModel: true);
|
||||
$this->dispatch('success', 'Saved successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
// Rollback on error
|
||||
$this->model->setRawAttributes($original);
|
||||
$this->model->save();
|
||||
$this->syncData(); // Reload from model
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Property Type Patterns
|
||||
|
||||
**Required Strings**
|
||||
```php
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $name; // No ?, no default, always has value
|
||||
```
|
||||
|
||||
**Nullable Strings**
|
||||
```php
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null; // ?, = null, can be empty
|
||||
```
|
||||
|
||||
**Booleans**
|
||||
```php
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isEnabled = false; // Always has default value
|
||||
```
|
||||
|
||||
**Integers with Constraints**
|
||||
```php
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $timeout; // Required
|
||||
|
||||
#[Validate(['integer', 'min:1', 'nullable'])]
|
||||
public ?int $port = null; // Nullable
|
||||
```
|
||||
|
||||
#### Testing Checklist
|
||||
|
||||
After creating a new component with syncData(), verify:
|
||||
|
||||
- [ ] All checkboxes save correctly (especially `instantSave` ones)
|
||||
- [ ] All form inputs persist to database
|
||||
- [ ] Custom encoded fields (like labels) display correctly if applicable
|
||||
- [ ] Form validation works for all fields
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Authorization checks work (`@can` directives and `authorize()` calls)
|
||||
- [ ] Error rollback works if exceptions occur
|
||||
- [ ] Related models save correctly if applicable (e.g., Application + ApplicationSetting)
|
||||
|
||||
#### Common Pitfalls to Avoid
|
||||
|
||||
1. **snake_case in component properties**: Always use camelCase for component properties (e.g., `$gitRepository` not `$git_repository`)
|
||||
2. **Missing #[Validate] attributes**: Every property should have validation attributes for type safety
|
||||
3. **Forgetting to call syncData()**: Must call `syncData()` in `mount()` to load initial data
|
||||
4. **Missing authorization**: Always use `authorize()` in methods and `canGate`/`canResource` in views
|
||||
5. **View binding mismatch**: Use camelCase in Blade (e.g., `id="gitRepository"` not `id="git_repository"`)
|
||||
6. **wire:model vs wire:model.live**: Use `.live` for `instantSave` checkboxes to avoid timing issues
|
||||
7. **Validation sync**: If using `rules()` method, keep it in sync with `#[Validate]` attributes
|
||||
8. **Related models**: Don't forget to save both main and related models in syncData() method
|
||||
|
||||
### Livewire Forms
|
||||
```php
|
||||
class ServerCreateForm extends Component
|
||||
{
|
||||
public $name;
|
||||
public $ip;
|
||||
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|min:3',
|
||||
'ip' => 'required|ip',
|
||||
];
|
||||
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Application\GenerateConfig;
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\Application;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public string $applicationId;
|
||||
|
||||
|
|
@ -23,94 +22,136 @@ class General extends Component
|
|||
|
||||
public Collection $services;
|
||||
|
||||
#[Validate('required|regex:/^[a-zA-Z0-9\s\-_.\/:()]+$/')]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
public string $git_repository;
|
||||
#[Validate(['required'])]
|
||||
public string $gitRepository;
|
||||
|
||||
public string $git_branch;
|
||||
#[Validate(['required'])]
|
||||
public string $gitBranch;
|
||||
|
||||
public ?string $git_commit_sha = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $gitCommitSha = null;
|
||||
|
||||
public ?string $install_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $installCommand = null;
|
||||
|
||||
public ?string $build_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $buildCommand = null;
|
||||
|
||||
public ?string $start_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $startCommand = null;
|
||||
|
||||
public string $build_pack;
|
||||
#[Validate(['required'])]
|
||||
public string $buildPack;
|
||||
|
||||
public string $static_image;
|
||||
#[Validate(['required'])]
|
||||
public string $staticImage;
|
||||
|
||||
public string $base_directory;
|
||||
#[Validate(['required'])]
|
||||
public string $baseDirectory;
|
||||
|
||||
public ?string $publish_directory = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $publishDirectory = null;
|
||||
|
||||
public ?string $ports_exposes = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $portsExposes = null;
|
||||
|
||||
public ?string $ports_mappings = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?string $custom_network_aliases = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customNetworkAliases = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfile = null;
|
||||
|
||||
public ?string $dockerfile_location = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfileLocation = null;
|
||||
|
||||
public ?string $dockerfile_target_build = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfileTargetBuild = null;
|
||||
|
||||
public ?string $docker_registry_image_name = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageName = null;
|
||||
|
||||
public ?string $docker_registry_image_tag = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageTag = null;
|
||||
|
||||
public ?string $docker_compose_location = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeLocation = null;
|
||||
|
||||
public ?string $docker_compose = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerCompose = null;
|
||||
|
||||
public ?string $docker_compose_raw = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeRaw = null;
|
||||
|
||||
public ?string $docker_compose_custom_start_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeCustomStartCommand = null;
|
||||
|
||||
public ?string $docker_compose_custom_build_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeCustomBuildCommand = null;
|
||||
|
||||
public ?string $custom_labels = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public ?string $custom_docker_run_options = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $preDeploymentCommand = null;
|
||||
|
||||
public ?string $pre_deployment_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $preDeploymentCommandContainer = null;
|
||||
|
||||
public ?string $pre_deployment_command_container = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $postDeploymentCommand = null;
|
||||
|
||||
public ?string $post_deployment_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $postDeploymentCommandContainer = null;
|
||||
|
||||
public ?string $post_deployment_command_container = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customNginxConfiguration = null;
|
||||
|
||||
public ?string $custom_nginx_configuration = null;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isStatic = false;
|
||||
|
||||
public bool $is_static = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isSpa = false;
|
||||
|
||||
public bool $is_spa = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isBuildServerEnabled = false;
|
||||
|
||||
public bool $is_build_server_enabled = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isPreserveRepositoryEnabled = false;
|
||||
|
||||
public bool $is_preserve_repository_enabled = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isContainerLabelEscapeEnabled = true;
|
||||
|
||||
public bool $is_container_label_escape_enabled = true;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isContainerLabelReadonlyEnabled = false;
|
||||
|
||||
public bool $is_container_label_readonly_enabled = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isHttpBasicAuthEnabled = false;
|
||||
|
||||
public bool $is_http_basic_auth_enabled = false;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $httpBasicAuthUsername = null;
|
||||
|
||||
public ?string $http_basic_auth_username = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $httpBasicAuthPassword = null;
|
||||
|
||||
public ?string $http_basic_auth_password = null;
|
||||
|
||||
public ?string $watch_paths = null;
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $watchPaths = null;
|
||||
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $redirect;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public $customLabels;
|
||||
|
||||
public bool $labelsChanged = false;
|
||||
|
|
@ -141,46 +182,46 @@ protected function rules(): array
|
|||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'fqdn' => 'nullable',
|
||||
'git_repository' => 'required',
|
||||
'git_branch' => 'required',
|
||||
'git_commit_sha' => 'nullable',
|
||||
'install_command' => 'nullable',
|
||||
'build_command' => 'nullable',
|
||||
'start_command' => 'nullable',
|
||||
'build_pack' => 'required',
|
||||
'static_image' => 'required',
|
||||
'base_directory' => 'required',
|
||||
'publish_directory' => 'nullable',
|
||||
'ports_exposes' => 'required',
|
||||
'ports_mappings' => 'nullable',
|
||||
'custom_network_aliases' => 'nullable',
|
||||
'gitRepository' => 'required',
|
||||
'gitBranch' => 'required',
|
||||
'gitCommitSha' => 'nullable',
|
||||
'installCommand' => 'nullable',
|
||||
'buildCommand' => 'nullable',
|
||||
'startCommand' => 'nullable',
|
||||
'buildPack' => 'required',
|
||||
'staticImage' => 'required',
|
||||
'baseDirectory' => 'required',
|
||||
'publishDirectory' => 'nullable',
|
||||
'portsExposes' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'customNetworkAliases' => 'nullable',
|
||||
'dockerfile' => 'nullable',
|
||||
'docker_registry_image_name' => 'nullable',
|
||||
'docker_registry_image_tag' => 'nullable',
|
||||
'dockerfile_location' => 'nullable',
|
||||
'docker_compose_location' => 'nullable',
|
||||
'docker_compose' => 'nullable',
|
||||
'docker_compose_raw' => 'nullable',
|
||||
'dockerfile_target_build' => 'nullable',
|
||||
'docker_compose_custom_start_command' => 'nullable',
|
||||
'docker_compose_custom_build_command' => 'nullable',
|
||||
'custom_labels' => 'nullable',
|
||||
'custom_docker_run_options' => 'nullable',
|
||||
'pre_deployment_command' => 'nullable',
|
||||
'pre_deployment_command_container' => 'nullable',
|
||||
'post_deployment_command' => 'nullable',
|
||||
'post_deployment_command_container' => 'nullable',
|
||||
'custom_nginx_configuration' => 'nullable',
|
||||
'is_static' => 'boolean|required',
|
||||
'is_spa' => 'boolean|required',
|
||||
'is_build_server_enabled' => 'boolean|required',
|
||||
'is_container_label_escape_enabled' => 'boolean|required',
|
||||
'is_container_label_readonly_enabled' => 'boolean|required',
|
||||
'is_preserve_repository_enabled' => 'boolean|required',
|
||||
'is_http_basic_auth_enabled' => 'boolean|required',
|
||||
'http_basic_auth_username' => 'string|nullable',
|
||||
'http_basic_auth_password' => 'string|nullable',
|
||||
'watch_paths' => 'nullable',
|
||||
'dockerRegistryImageName' => 'nullable',
|
||||
'dockerRegistryImageTag' => 'nullable',
|
||||
'dockerfileLocation' => 'nullable',
|
||||
'dockerComposeLocation' => 'nullable',
|
||||
'dockerCompose' => 'nullable',
|
||||
'dockerComposeRaw' => 'nullable',
|
||||
'dockerfileTargetBuild' => 'nullable',
|
||||
'dockerComposeCustomStartCommand' => 'nullable',
|
||||
'dockerComposeCustomBuildCommand' => 'nullable',
|
||||
'customLabels' => 'nullable',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'preDeploymentCommand' => 'nullable',
|
||||
'preDeploymentCommandContainer' => 'nullable',
|
||||
'postDeploymentCommand' => 'nullable',
|
||||
'postDeploymentCommandContainer' => 'nullable',
|
||||
'customNginxConfiguration' => 'nullable',
|
||||
'isStatic' => 'boolean|required',
|
||||
'isSpa' => 'boolean|required',
|
||||
'isBuildServerEnabled' => 'boolean|required',
|
||||
'isContainerLabelEscapeEnabled' => 'boolean|required',
|
||||
'isContainerLabelReadonlyEnabled' => 'boolean|required',
|
||||
'isPreserveRepositoryEnabled' => 'boolean|required',
|
||||
'isHttpBasicAuthEnabled' => 'boolean|required',
|
||||
'httpBasicAuthUsername' => 'string|nullable',
|
||||
'httpBasicAuthPassword' => 'string|nullable',
|
||||
'watchPaths' => 'nullable',
|
||||
'redirect' => 'string|required',
|
||||
];
|
||||
}
|
||||
|
|
@ -193,26 +234,26 @@ protected function messages(): array
|
|||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'git_repository.required' => 'The Git Repository field is required.',
|
||||
'git_branch.required' => 'The Git Branch field is required.',
|
||||
'build_pack.required' => 'The Build Pack field is required.',
|
||||
'static_image.required' => 'The Static Image field is required.',
|
||||
'base_directory.required' => 'The Base Directory field is required.',
|
||||
'ports_exposes.required' => 'The Exposed Ports field is required.',
|
||||
'is_static.required' => 'The Static setting is required.',
|
||||
'is_static.boolean' => 'The Static setting must be true or false.',
|
||||
'is_spa.required' => 'The SPA setting is required.',
|
||||
'is_spa.boolean' => 'The SPA setting must be true or false.',
|
||||
'is_build_server_enabled.required' => 'The Build Server setting is required.',
|
||||
'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
|
||||
'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
|
||||
'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||
'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
|
||||
'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||
'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
|
||||
'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||
'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||
'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||
'gitRepository.required' => 'The Git Repository field is required.',
|
||||
'gitBranch.required' => 'The Git Branch field is required.',
|
||||
'buildPack.required' => 'The Build Pack field is required.',
|
||||
'staticImage.required' => 'The Static Image field is required.',
|
||||
'baseDirectory.required' => 'The Base Directory field is required.',
|
||||
'portsExposes.required' => 'The Exposed Ports field is required.',
|
||||
'isStatic.required' => 'The Static setting is required.',
|
||||
'isStatic.boolean' => 'The Static setting must be true or false.',
|
||||
'isSpa.required' => 'The SPA setting is required.',
|
||||
'isSpa.boolean' => 'The SPA setting must be true or false.',
|
||||
'isBuildServerEnabled.required' => 'The Build Server setting is required.',
|
||||
'isBuildServerEnabled.boolean' => 'The Build Server setting must be true or false.',
|
||||
'isContainerLabelEscapeEnabled.required' => 'The Container Label Escape setting is required.',
|
||||
'isContainerLabelEscapeEnabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||
'isContainerLabelReadonlyEnabled.required' => 'The Container Label Readonly setting is required.',
|
||||
'isContainerLabelReadonlyEnabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||
'isPreserveRepositoryEnabled.required' => 'The Preserve Repository setting is required.',
|
||||
'isPreserveRepositoryEnabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||
'isHttpBasicAuthEnabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||
'isHttpBasicAuthEnabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||
'redirect.required' => 'The Redirect setting is required.',
|
||||
'redirect.string' => 'The Redirect setting must be a string.',
|
||||
]
|
||||
|
|
@ -220,43 +261,43 @@ protected function messages(): array
|
|||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'application.name' => 'name',
|
||||
'application.description' => 'description',
|
||||
'application.fqdn' => 'FQDN',
|
||||
'application.git_repository' => 'Git repository',
|
||||
'application.git_branch' => 'Git branch',
|
||||
'application.git_commit_sha' => 'Git commit SHA',
|
||||
'application.install_command' => 'Install command',
|
||||
'application.build_command' => 'Build command',
|
||||
'application.start_command' => 'Start command',
|
||||
'application.build_pack' => 'Build pack',
|
||||
'application.static_image' => 'Static image',
|
||||
'application.base_directory' => 'Base directory',
|
||||
'application.publish_directory' => 'Publish directory',
|
||||
'application.ports_exposes' => 'Ports exposes',
|
||||
'application.ports_mappings' => 'Ports mappings',
|
||||
'application.dockerfile' => 'Dockerfile',
|
||||
'application.docker_registry_image_name' => 'Docker registry image name',
|
||||
'application.docker_registry_image_tag' => 'Docker registry image tag',
|
||||
'application.dockerfile_location' => 'Dockerfile location',
|
||||
'application.docker_compose_location' => 'Docker compose location',
|
||||
'application.docker_compose' => 'Docker compose',
|
||||
'application.docker_compose_raw' => 'Docker compose raw',
|
||||
'application.custom_labels' => 'Custom labels',
|
||||
'application.dockerfile_target_build' => 'Dockerfile target build',
|
||||
'application.custom_docker_run_options' => 'Custom docker run commands',
|
||||
'application.custom_network_aliases' => 'Custom docker network aliases',
|
||||
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
|
||||
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
|
||||
'application.custom_nginx_configuration' => 'Custom Nginx configuration',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
'application.settings.is_spa' => 'Is SPA',
|
||||
'application.settings.is_build_server_enabled' => 'Is build server enabled',
|
||||
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
|
||||
'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
|
||||
'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled',
|
||||
'application.watch_paths' => 'Watch paths',
|
||||
'application.redirect' => 'Redirect',
|
||||
'name' => 'name',
|
||||
'description' => 'description',
|
||||
'fqdn' => 'FQDN',
|
||||
'gitRepository' => 'Git repository',
|
||||
'gitBranch' => 'Git branch',
|
||||
'gitCommitSha' => 'Git commit SHA',
|
||||
'installCommand' => 'Install command',
|
||||
'buildCommand' => 'Build command',
|
||||
'startCommand' => 'Start command',
|
||||
'buildPack' => 'Build pack',
|
||||
'staticImage' => 'Static image',
|
||||
'baseDirectory' => 'Base directory',
|
||||
'publishDirectory' => 'Publish directory',
|
||||
'portsExposes' => 'Ports exposes',
|
||||
'portsMappings' => 'Ports mappings',
|
||||
'dockerfile' => 'Dockerfile',
|
||||
'dockerRegistryImageName' => 'Docker registry image name',
|
||||
'dockerRegistryImageTag' => 'Docker registry image tag',
|
||||
'dockerfileLocation' => 'Dockerfile location',
|
||||
'dockerComposeLocation' => 'Docker compose location',
|
||||
'dockerCompose' => 'Docker compose',
|
||||
'dockerComposeRaw' => 'Docker compose raw',
|
||||
'customLabels' => 'Custom labels',
|
||||
'dockerfileTargetBuild' => 'Dockerfile target build',
|
||||
'customDockerRunOptions' => 'Custom docker run commands',
|
||||
'customNetworkAliases' => 'Custom docker network aliases',
|
||||
'dockerComposeCustomStartCommand' => 'Docker compose custom start command',
|
||||
'dockerComposeCustomBuildCommand' => 'Docker compose custom build command',
|
||||
'customNginxConfiguration' => 'Custom Nginx configuration',
|
||||
'isStatic' => 'Is static',
|
||||
'isSpa' => 'Is SPA',
|
||||
'isBuildServerEnabled' => 'Is build server enabled',
|
||||
'isContainerLabelEscapeEnabled' => 'Is container label escape enabled',
|
||||
'isContainerLabelReadonlyEnabled' => 'Is container label readonly',
|
||||
'isPreserveRepositoryEnabled' => 'Is preserve repository enabled',
|
||||
'watchPaths' => 'Watch paths',
|
||||
'redirect' => 'Redirect',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
|
@ -266,14 +307,14 @@ public function mount()
|
|||
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.');
|
||||
// Still sync data even if parse fails, so form fields are populated
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
// Still sync data even on error, so form fields are populated
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
// Only update if user has permission
|
||||
|
|
@ -325,57 +366,112 @@ public function mount()
|
|||
|
||||
// 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();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
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',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Application properties
|
||||
$this->application->name = $this->name;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->git_repository = $this->gitRepository;
|
||||
$this->application->git_branch = $this->gitBranch;
|
||||
$this->application->git_commit_sha = $this->gitCommitSha;
|
||||
$this->application->install_command = $this->installCommand;
|
||||
$this->application->build_command = $this->buildCommand;
|
||||
$this->application->start_command = $this->startCommand;
|
||||
$this->application->build_pack = $this->buildPack;
|
||||
$this->application->static_image = $this->staticImage;
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
$this->application->ports_exposes = $this->portsExposes;
|
||||
$this->application->ports_mappings = $this->portsMappings;
|
||||
$this->application->custom_network_aliases = $this->customNetworkAliases;
|
||||
$this->application->dockerfile = $this->dockerfile;
|
||||
$this->application->dockerfile_location = $this->dockerfileLocation;
|
||||
$this->application->dockerfile_target_build = $this->dockerfileTargetBuild;
|
||||
$this->application->docker_registry_image_name = $this->dockerRegistryImageName;
|
||||
$this->application->docker_registry_image_tag = $this->dockerRegistryImageTag;
|
||||
$this->application->docker_compose_location = $this->dockerComposeLocation;
|
||||
$this->application->docker_compose = $this->dockerCompose;
|
||||
$this->application->docker_compose_raw = $this->dockerComposeRaw;
|
||||
$this->application->docker_compose_custom_start_command = $this->dockerComposeCustomStartCommand;
|
||||
$this->application->docker_compose_custom_build_command = $this->dockerComposeCustomBuildCommand;
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->application->pre_deployment_command = $this->preDeploymentCommand;
|
||||
$this->application->pre_deployment_command_container = $this->preDeploymentCommandContainer;
|
||||
$this->application->post_deployment_command = $this->postDeploymentCommand;
|
||||
$this->application->post_deployment_command_container = $this->postDeploymentCommandContainer;
|
||||
$this->application->custom_nginx_configuration = $this->customNginxConfiguration;
|
||||
$this->application->is_http_basic_auth_enabled = $this->isHttpBasicAuthEnabled;
|
||||
$this->application->http_basic_auth_username = $this->httpBasicAuthUsername;
|
||||
$this->application->http_basic_auth_password = $this->httpBasicAuthPassword;
|
||||
$this->application->watch_paths = $this->watchPaths;
|
||||
$this->application->redirect = $this->redirect;
|
||||
|
||||
// Application settings properties
|
||||
$this->application->settings->is_static = $this->isStatic;
|
||||
$this->application->settings->is_spa = $this->isSpa;
|
||||
$this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled;
|
||||
$this->application->settings->is_preserve_repository_enabled = $this->isPreserveRepositoryEnabled;
|
||||
$this->application->settings->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled;
|
||||
$this->application->settings->is_container_label_readonly_enabled = $this->isContainerLabelReadonlyEnabled;
|
||||
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
// From model to properties
|
||||
$this->name = $this->application->name;
|
||||
$this->description = $this->application->description;
|
||||
$this->fqdn = $this->application->fqdn;
|
||||
$this->gitRepository = $this->application->git_repository;
|
||||
$this->gitBranch = $this->application->git_branch;
|
||||
$this->gitCommitSha = $this->application->git_commit_sha;
|
||||
$this->installCommand = $this->application->install_command;
|
||||
$this->buildCommand = $this->application->build_command;
|
||||
$this->startCommand = $this->application->start_command;
|
||||
$this->buildPack = $this->application->build_pack;
|
||||
$this->staticImage = $this->application->static_image;
|
||||
$this->baseDirectory = $this->application->base_directory;
|
||||
$this->publishDirectory = $this->application->publish_directory;
|
||||
$this->portsExposes = $this->application->ports_exposes;
|
||||
$this->portsMappings = $this->application->ports_mappings;
|
||||
$this->customNetworkAliases = $this->application->custom_network_aliases;
|
||||
$this->dockerfile = $this->application->dockerfile;
|
||||
$this->dockerfileLocation = $this->application->dockerfile_location;
|
||||
$this->dockerfileTargetBuild = $this->application->dockerfile_target_build;
|
||||
$this->dockerRegistryImageName = $this->application->docker_registry_image_name;
|
||||
$this->dockerRegistryImageTag = $this->application->docker_registry_image_tag;
|
||||
$this->dockerComposeLocation = $this->application->docker_compose_location;
|
||||
$this->dockerCompose = $this->application->docker_compose;
|
||||
$this->dockerComposeRaw = $this->application->docker_compose_raw;
|
||||
$this->dockerComposeCustomStartCommand = $this->application->docker_compose_custom_start_command;
|
||||
$this->dockerComposeCustomBuildCommand = $this->application->docker_compose_custom_build_command;
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
$this->customDockerRunOptions = $this->application->custom_docker_run_options;
|
||||
$this->preDeploymentCommand = $this->application->pre_deployment_command;
|
||||
$this->preDeploymentCommandContainer = $this->application->pre_deployment_command_container;
|
||||
$this->postDeploymentCommand = $this->application->post_deployment_command;
|
||||
$this->postDeploymentCommandContainer = $this->application->post_deployment_command_container;
|
||||
$this->customNginxConfiguration = $this->application->custom_nginx_configuration;
|
||||
$this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled;
|
||||
$this->httpBasicAuthUsername = $this->application->http_basic_auth_username;
|
||||
$this->httpBasicAuthPassword = $this->application->http_basic_auth_password;
|
||||
$this->watchPaths = $this->application->watch_paths;
|
||||
$this->redirect = $this->application->redirect;
|
||||
|
||||
// Application settings properties
|
||||
$this->isStatic = $this->application->settings->is_static;
|
||||
$this->isSpa = $this->application->settings->is_spa;
|
||||
$this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled;
|
||||
$this->isPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->isContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$this->isContainerLabelReadonlyEnabled = $this->application->settings->is_container_label_readonly_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
|
@ -387,7 +483,7 @@ public function instantSave()
|
|||
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
|
||||
if ($this->application->settings->isDirty('is_spa')) {
|
||||
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
|
||||
|
|
@ -395,24 +491,27 @@ public function instantSave()
|
|||
if ($this->application->isDirty('is_http_basic_auth_enabled')) {
|
||||
$this->application->save();
|
||||
}
|
||||
|
||||
$this->application->settings->save();
|
||||
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
|
||||
$this->syncData();
|
||||
|
||||
// If port_exposes changed, reset default labels
|
||||
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) {
|
||||
if ($this->is_preserve_repository_enabled === false) {
|
||||
if ($oldIsPreserveRepositoryEnabled !== $this->isPreserveRepositoryEnabled) {
|
||||
if ($this->isPreserveRepositoryEnabled === false) {
|
||||
$this->application->fileStorages->each(function ($storage) {
|
||||
$storage->is_based_on_git = $this->is_preserve_repository_enabled;
|
||||
$storage->is_based_on_git = $this->isPreserveRepositoryEnabled;
|
||||
$storage->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($this->is_container_label_readonly_enabled) {
|
||||
if ($this->isContainerLabelReadonlyEnabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -441,7 +540,7 @@ public function loadComposeFile($isInit = false, $showToast = true)
|
|||
|
||||
// Sync the docker_compose_raw from the model to the component property
|
||||
// This ensures the Monaco editor displays the loaded compose file
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots and dashes to use underscores for HTML form binding
|
||||
|
|
@ -507,7 +606,7 @@ public function generateDomain(string $serviceName)
|
|||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
}
|
||||
}
|
||||
|
|
@ -527,24 +626,24 @@ public function updatedBuildPack()
|
|||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
// User doesn't have permission, revert the change and return
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync property to model before checking/modifying
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
|
||||
if ($this->build_pack !== 'nixpacks') {
|
||||
$this->is_static = false;
|
||||
if ($this->buildPack !== 'nixpacks') {
|
||||
$this->isStatic = false;
|
||||
$this->application->settings->is_static = false;
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
$this->ports_exposes = 3000;
|
||||
$this->application->ports_exposes = 3000;
|
||||
$this->portsExposes = '3000';
|
||||
$this->application->ports_exposes = '3000';
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
// Only update if user has permission
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
|
@ -567,9 +666,9 @@ public function updatedBuildPack()
|
|||
$this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
|
||||
}
|
||||
}
|
||||
if ($this->build_pack === 'static') {
|
||||
$this->ports_exposes = 80;
|
||||
$this->application->ports_exposes = 80;
|
||||
if ($this->buildPack === 'static') {
|
||||
$this->portsExposes = '80';
|
||||
$this->application->ports_exposes = '80';
|
||||
$this->resetDefaultLabels(false);
|
||||
$this->generateNginxConfiguration();
|
||||
}
|
||||
|
|
@ -586,10 +685,10 @@ public function getWildcardDomain()
|
|||
if ($server) {
|
||||
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
|
||||
$this->fqdn = $fqdn;
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$this->resetDefaultLabels();
|
||||
$this->dispatch('success', 'Wildcard domain generated.');
|
||||
}
|
||||
|
|
@ -603,11 +702,11 @@ public function generateNginxConfiguration($type = 'static')
|
|||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->custom_nginx_configuration = defaultNginxConfiguration($type);
|
||||
$this->syncToModel();
|
||||
$this->customNginxConfiguration = defaultNginxConfiguration($type);
|
||||
$this->syncData(toModel: true);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$this->dispatch('success', 'Nginx configuration generated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
@ -617,16 +716,15 @@ public function generateNginxConfiguration($type = 'static')
|
|||
public function resetDefaultLabels($manualReset = false)
|
||||
{
|
||||
try {
|
||||
if (! $this->is_container_label_readonly_enabled && ! $manualReset) {
|
||||
if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) {
|
||||
return;
|
||||
}
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
$this->custom_labels = base64_encode($this->customLabels);
|
||||
$this->syncToModel();
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$this->syncData();
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->loadComposeFile(showToast: false);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
|
|
@ -722,7 +820,7 @@ public function submit($showToaster = true)
|
|||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
|
||||
if ($this->application->isDirty('redirect')) {
|
||||
$this->setRedirect();
|
||||
|
|
@ -742,42 +840,42 @@ public function submit($showToaster = true)
|
|||
$this->application->save();
|
||||
}
|
||||
|
||||
if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) {
|
||||
if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
|
||||
$compose_return = $this->loadComposeFile(showToast: false);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
if ($this->build_pack === 'dockerimage') {
|
||||
if ($this->buildPack === 'dockerimage') {
|
||||
$this->validate([
|
||||
'docker_registry_image_name' => 'required',
|
||||
'dockerRegistryImageName' => 'required',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->custom_docker_run_options) {
|
||||
$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 ($this->customDockerRunOptions) {
|
||||
$this->customDockerRunOptions = str($this->customDockerRunOptions)->trim()->toString();
|
||||
$this->application->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
}
|
||||
if ($this->dockerfile) {
|
||||
$port = get_port_from_dockerfile($this->dockerfile);
|
||||
if ($port && ! $this->ports_exposes) {
|
||||
$this->ports_exposes = $port;
|
||||
if ($port && ! $this->portsExposes) {
|
||||
$this->portsExposes = $port;
|
||||
$this->application->ports_exposes = $port;
|
||||
}
|
||||
}
|
||||
if ($this->base_directory && $this->base_directory !== '/') {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
$this->application->base_directory = $this->base_directory;
|
||||
if ($this->baseDirectory && $this->baseDirectory !== '/') {
|
||||
$this->baseDirectory = rtrim($this->baseDirectory, '/');
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
}
|
||||
if ($this->publish_directory && $this->publish_directory !== '/') {
|
||||
$this->publish_directory = rtrim($this->publish_directory, '/');
|
||||
$this->application->publish_directory = $this->publish_directory;
|
||||
if ($this->publishDirectory && $this->publishDirectory !== '/') {
|
||||
$this->publishDirectory = rtrim($this->publishDirectory, '/');
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
foreach ($this->parsedServiceDomains as $service) {
|
||||
|
|
@ -809,11 +907,11 @@ public function submit($showToaster = true)
|
|||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@
|
|||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class EditDomain extends Component
|
||||
{
|
||||
use SynchronizesModelData;
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $applicationId;
|
||||
|
||||
public ServiceApplication $application;
|
||||
|
|
@ -20,6 +22,7 @@ class EditDomain extends Component
|
|||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -28,16 +31,24 @@ class EditDomain extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->application = ServiceApplication::query()->findOrFail($this->applicationId);
|
||||
$this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId);
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
return [
|
||||
'fqdn' => 'application.fqdn',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->fqdn = $this->application->fqdn;
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmDomainUsage()
|
||||
|
|
@ -64,8 +75,8 @@ public function submit()
|
|||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// Sync to model for domain conflict check
|
||||
$this->syncToModel();
|
||||
// Sync to model for domain conflict check (without validation)
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application);
|
||||
|
|
@ -83,7 +94,7 @@ public function submit()
|
|||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
updateCompose($this->application);
|
||||
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.');
|
||||
|
|
@ -96,7 +107,7 @@ public function submit()
|
|||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
|
|
@ -19,11 +18,12 @@
|
|||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class FileStorage extends Component
|
||||
{
|
||||
use AuthorizesRequests, SynchronizesModelData;
|
||||
use AuthorizesRequests;
|
||||
|
||||
public LocalFileVolume $fileStorage;
|
||||
|
||||
|
|
@ -37,8 +37,10 @@ class FileStorage extends Component
|
|||
|
||||
public bool $isReadOnly = false;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $content = null;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $isBasedOnGit = false;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -61,15 +63,24 @@ public function mount()
|
|||
}
|
||||
|
||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
return [
|
||||
'content' => 'fileStorage.content',
|
||||
'isBasedOnGit' => 'fileStorage.is_based_on_git',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->fileStorage->content = $this->content;
|
||||
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
|
||||
|
||||
$this->fileStorage->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->content = $this->fileStorage->content;
|
||||
$this->isBasedOnGit = $this->fileStorage->is_based_on_git;
|
||||
}
|
||||
}
|
||||
|
||||
public function convertToDirectory()
|
||||
|
|
@ -96,7 +107,7 @@ public function loadStorageOnServer()
|
|||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$this->dispatch('success', 'File storage loaded from server.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
@ -165,14 +176,16 @@ public function submit()
|
|||
if ($this->fileStorage->is_directory) {
|
||||
$this->content = null;
|
||||
}
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->fileStorage->content = $this->content;
|
||||
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
$this->dispatch('success', 'File updated.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->fileStorage->setRawAttributes($original);
|
||||
$this->fileStorage->save();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,19 @@
|
|||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class ServiceApplicationView extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public ServiceApplication $application;
|
||||
|
||||
|
|
@ -31,20 +30,28 @@ class ServiceApplicationView extends Component
|
|||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $humanName = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $image = null;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $excludeFromStatus = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isGzipEnabled = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isStripprefixEnabled = false;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -79,7 +86,15 @@ public function instantSaveAdvanced()
|
|||
|
||||
return;
|
||||
}
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->application->human_name = $this->humanName;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->image = $this->image;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -114,24 +129,39 @@ public function mount()
|
|||
try {
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
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',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->application->human_name = $this->humanName;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->image = $this->image;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->humanName = $this->application->human_name;
|
||||
$this->description = $this->application->description;
|
||||
$this->fqdn = $this->application->fqdn;
|
||||
$this->image = $this->application->image;
|
||||
$this->excludeFromStatus = $this->application->exclude_from_status;
|
||||
$this->isLogDrainEnabled = $this->application->is_log_drain_enabled;
|
||||
$this->isGzipEnabled = $this->application->is_gzip_enabled;
|
||||
$this->isStripprefixEnabled = $this->application->is_stripprefix_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public function convertToDatabase()
|
||||
|
|
@ -193,8 +223,15 @@ public function submit()
|
|||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// Sync to model for domain conflict check
|
||||
$this->syncToModel();
|
||||
// Sync to model for domain conflict check (without validation)
|
||||
$this->application->human_name = $this->humanName;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->image = $this->image;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application);
|
||||
|
|
@ -212,7 +249,7 @@ public function submit()
|
|||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
updateCompose($this->application);
|
||||
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.');
|
||||
|
|
@ -224,7 +261,7 @@ public function submit()
|
|||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -2,42 +2,54 @@
|
|||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class HealthChecks extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public $resource;
|
||||
|
||||
// Explicit properties
|
||||
#[Validate(['boolean'])]
|
||||
public bool $healthCheckEnabled = false;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckMethod;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckScheme;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckHost;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $healthCheckPort = null;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckPath;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
public int $healthCheckReturnCode;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $healthCheckResponseText = null;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckInterval;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckTimeout;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckRetries;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
public int $healthCheckStartPeriod;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $customHealthcheckFound = false;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -56,36 +68,69 @@ class HealthChecks extends Component
|
|||
'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();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
|
||||
$this->resource->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
||||
$this->healthCheckMethod = $this->resource->health_check_method;
|
||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||
$this->healthCheckHost = $this->resource->health_check_host;
|
||||
$this->healthCheckPort = $this->resource->health_check_port;
|
||||
$this->healthCheckPath = $this->resource->health_check_path;
|
||||
$this->healthCheckReturnCode = $this->resource->health_check_return_code;
|
||||
$this->healthCheckResponseText = $this->resource->health_check_response_text;
|
||||
$this->healthCheckInterval = $this->resource->health_check_interval;
|
||||
$this->healthCheckTimeout = $this->resource->health_check_timeout;
|
||||
$this->healthCheckRetries = $this->resource->health_check_retries;
|
||||
$this->healthCheckStartPeriod = $this->resource->health_check_start_period;
|
||||
$this->customHealthcheckFound = $this->resource->custom_healthcheck_found;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
}
|
||||
|
|
@ -96,7 +141,20 @@ public function submit()
|
|||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -111,7 +169,20 @@ public function toggleHealthcheck()
|
|||
$wasEnabled = $this->healthCheckEnabled;
|
||||
$this->healthCheckEnabled = ! $this->healthCheckEnabled;
|
||||
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
|
||||
if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,14 @@
|
|||
|
||||
class ApplicationSetting extends Model
|
||||
{
|
||||
protected $cast = [
|
||||
protected $casts = [
|
||||
'is_static' => 'boolean',
|
||||
'is_spa' => 'boolean',
|
||||
'is_build_server_enabled' => 'boolean',
|
||||
'is_preserve_repository_enabled' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
'is_container_label_readonly_enabled' => 'boolean',
|
||||
'use_build_secrets' => 'boolean',
|
||||
'is_auto_deploy_enabled' => 'boolean',
|
||||
'is_force_https_enabled' => 'boolean',
|
||||
'is_debug_enabled' => 'boolean',
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="build_pack"
|
||||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="buildPack"
|
||||
label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="static">Static</option>
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<option value="dockercompose">Docker Compose</option>
|
||||
</x-forms.select>
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.select x-bind:disabled="!canUpdate" id="static_image"
|
||||
<x-forms.select x-bind:disabled="!canUpdate" id="staticImage"
|
||||
label="Static Image" required>
|
||||
<option value="nginx:alpine">nginx:alpine</option>
|
||||
<option disabled value="apache:alpine">apache:alpine</option>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
</div>
|
||||
@endif
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.textarea id="custom_nginx_configuration"
|
||||
<x-forms.textarea id="customNginxConfiguration"
|
||||
placeholder="Empty means default configuration will be used." label="Custom Nginx Configuration"
|
||||
helper="You can add custom Nginx configuration here." x-bind:disabled="!canUpdate" />
|
||||
@can('update', $application)
|
||||
|
|
@ -77,13 +77,13 @@
|
|||
@endif
|
||||
<div class="w-96 pb-6">
|
||||
@if ($application->could_set_build_commands())
|
||||
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?"
|
||||
<x-forms.checkbox instantSave id="isStatic" 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."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if ($application->settings->is_static && $application->build_pack !== 'static')
|
||||
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
|
||||
helper="If your application is a SPA, enable this." id="is_spa" instantSave
|
||||
helper="If your application is a SPA, enable this." id="isSpa" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -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">
|
||||
@if ($application->build_pack === 'dockerimage')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.input required id="docker_registry_image_name" label="Docker Image"
|
||||
<x-forms.input required id="dockerRegistryImageName" label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||
<x-forms.input id="dockerRegistryImageTag" label="Docker Image Tag or Hash"
|
||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input id="docker_registry_image_name" label="Docker Image"
|
||||
<x-forms.input id="dockerRegistryImageName" label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||
<x-forms.input id="dockerRegistryImageTag" label="Docker Image Tag or Hash"
|
||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
|
|
@ -181,18 +181,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
$application->destination->server->isSwarm() ||
|
||||
$application->additional_servers->count() > 0 ||
|
||||
$application->settings->is_build_server_enabled)
|
||||
<x-forms.input id="docker_registry_image_name" required label="Docker Image"
|
||||
<x-forms.input id="dockerRegistryImageName" required label="Docker Image"
|
||||
placeholder="Required!" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag"
|
||||
<x-forms.input id="dockerRegistryImageTag"
|
||||
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"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input id="docker_registry_image_name"
|
||||
<x-forms.input id="dockerRegistryImageName"
|
||||
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."
|
||||
label="Docker Image" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag"
|
||||
<x-forms.input id="dockerRegistryImageTag"
|
||||
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."
|
||||
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
|
||||
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"
|
||||
id="custom_docker_run_options" label="Custom Docker Options"
|
||||
id="customDockerRunOptions" label="Custom Docker Options"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
@if ($application->could_set_build_commands())
|
||||
@if ($application->build_pack === 'nixpacks')
|
||||
<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"
|
||||
id="install_command" label="Install Command"
|
||||
id="installCommand" label="Install Command"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
id="build_command" label="Build Command"
|
||||
id="buildCommand" label="Build Command"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
id="start_command" label="Start Command"
|
||||
id="startCommand" label="Start Command"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
<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
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/"
|
||||
id="base_directory" label="Base Directory"
|
||||
id="baseDirectory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="/docker-compose.yaml"
|
||||
id="docker_compose_location" label="Docker Compose Location"
|
||||
id="dockerComposeLocation" 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>" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave
|
||||
id="is_preserve_repository_enabled"
|
||||
id="isPreserveRepositoryEnabled"
|
||||
label="Preserve Repository During Deployment"
|
||||
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
|
||||
x-bind:disabled="shouldDisable()" />
|
||||
|
|
@ -261,12 +261,12 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<div class="flex gap-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="docker compose build"
|
||||
id="docker_compose_custom_build_command"
|
||||
id="dockerComposeCustomBuildCommand"
|
||||
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" />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="docker compose up -d"
|
||||
id="docker_compose_custom_start_command"
|
||||
id="dockerComposeCustomStartCommand"
|
||||
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" />
|
||||
</div>
|
||||
|
|
@ -274,36 +274,36 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<div class="pt-4">
|
||||
<x-forms.textarea
|
||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||
placeholder="services/api/**" id="watch_paths"
|
||||
placeholder="services/api/**" id="watchPaths"
|
||||
label="Watch Paths" x-bind:disabled="shouldDisable()" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input placeholder="/" id="base_directory"
|
||||
<x-forms.input placeholder="/" id="baseDirectory"
|
||||
label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
|
||||
<x-forms.input placeholder="/Dockerfile" id="dockerfile_location"
|
||||
<x-forms.input placeholder="/Dockerfile" id="dockerfileLocation"
|
||||
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>"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
|
||||
@if ($application->build_pack === 'dockerfile')
|
||||
<x-forms.input id="dockerfile_target_build"
|
||||
<x-forms.input id="dockerfileTargetBuild"
|
||||
label="Docker Build Stage Target"
|
||||
helper="Useful if you have multi-staged dockerfile."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if ($application->could_set_build_commands())
|
||||
@if ($application->settings->is_static)
|
||||
<x-forms.input placeholder="/dist" id="publish_directory"
|
||||
<x-forms.input placeholder="/dist" id="publishDirectory"
|
||||
label="Publish Directory" required x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="/" id="publish_directory"
|
||||
<x-forms.input placeholder="/" id="publishDirectory"
|
||||
label="Publish Directory" x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@endif
|
||||
|
|
@ -313,21 +313,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<div class="pb-4">
|
||||
<x-forms.textarea
|
||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||
placeholder="src/pages/**" id="watch_paths"
|
||||
placeholder="src/pages/**" id="watchPaths"
|
||||
label="Watch Paths" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
<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>"
|
||||
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="custom_docker_run_options" label="Custom Docker Options"
|
||||
id="customDockerRunOptions" label="Custom Docker Options"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<div class="pt-2 w-96">
|
||||
<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>."
|
||||
instantSave id="is_build_server_enabled"
|
||||
instantSave id="isBuildServerEnabled"
|
||||
label="Use a Build Server?" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -344,18 +344,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
@endcan
|
||||
</div>
|
||||
@if ($application->settings->is_raw_compose_deployment_enabled)
|
||||
<x-forms.textarea rows="10" readonly id="docker_compose_raw"
|
||||
<x-forms.textarea rows="10" readonly id="dockerComposeRaw"
|
||||
label="Docker Compose Content (applicationId: {{ $application->id }})"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
@else
|
||||
@if ((int) $application->compose_parsing_version >= 3)
|
||||
<x-forms.textarea rows="10" readonly id="docker_compose_raw"
|
||||
<x-forms.textarea rows="10" readonly id="dockerComposeRaw"
|
||||
label="Docker Compose Content (raw)"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
@endif
|
||||
<x-forms.textarea rows="10" readonly id="docker_compose"
|
||||
<x-forms.textarea rows="10" readonly id="dockerCompose"
|
||||
label="Docker Compose Content"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
|
|
@ -363,11 +363,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<div class="w-96">
|
||||
<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."
|
||||
id="is_container_label_escape_enabled" instantSave
|
||||
id="isContainerLabelEscapeEnabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
{{-- <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)."
|
||||
id="is_container_label_readonly_enabled" instantSave></x-forms.checkbox> --}}
|
||||
id="isContainerLabelReadonlyEnabled" instantSave></x-forms.checkbox> --}}
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->dockerfile)
|
||||
|
|
@ -378,30 +378,30 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<h3 class="pt-8">Network</h3>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.input id="ports_exposes" label="Ports Exposes" readonly
|
||||
<x-forms.input id="portsExposes" label="Ports Exposes" readonly
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
@if ($application->settings->is_container_label_readonly_enabled === false)
|
||||
<x-forms.input placeholder="3000,3001" id="ports_exposes"
|
||||
<x-forms.input placeholder="3000,3001" id="portsExposes"
|
||||
label="Ports Exposes" readonly
|
||||
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="3000,3001" id="ports_exposes"
|
||||
<x-forms.input placeholder="3000,3001" id="portsExposes"
|
||||
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."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.input placeholder="3000:3000" id="ports_mappings" label="Ports Mappings"
|
||||
<x-forms.input placeholder="3000:3000" id="portsMappings" 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."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.input id="custom_network_aliases" label="Network Aliases"
|
||||
<x-forms.input id="customNetworkAliases" 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"
|
||||
wire:model="custom_network_aliases" x-bind:disabled="!canUpdate" />
|
||||
wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -409,14 +409,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
||||
label="Enable" id="is_http_basic_auth_enabled"
|
||||
label="Enable" id="isHttpBasicAuthEnabled"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@if ($application->is_http_basic_auth_enabled)
|
||||
<div class="flex gap-2 py-2">
|
||||
<x-forms.input id="http_basic_auth_username" label="Username" required
|
||||
<x-forms.input id="httpBasicAuthUsername" label="Username" required
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="http_basic_auth_password" type="password" label="Password"
|
||||
<x-forms.input id="httpBasicAuthPassword" type="password" label="Password"
|
||||
required x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -432,11 +432,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<div class="w-96">
|
||||
<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)."
|
||||
id="is_container_label_readonly_enabled" instantSave
|
||||
id="isContainerLabelReadonlyEnabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
<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."
|
||||
id="is_container_label_escape_enabled" instantSave
|
||||
id="isContainerLabelEscapeEnabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
</div>
|
||||
@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>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||
id="pre_deployment_command" label="Pre-deployment "
|
||||
id="preDeploymentCommand" 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." />
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="pre_deployment_command_container"
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="preDeploymentCommandContainer"
|
||||
label="Container Name"
|
||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||
id="post_deployment_command" label="Post-deployment "
|
||||
id="postDeploymentCommand" 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." />
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
id="post_deployment_command_container" label="Container Name"
|
||||
id="postDeploymentCommandContainer" label="Container Name"
|
||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
105
tests/Unit/ApplicationSettingStaticCastTest.php
Normal file
105
tests/Unit/ApplicationSettingStaticCastTest.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Tests for ApplicationSetting model boolean casting
|
||||
*
|
||||
* NOTE: These tests verify that the is_static field properly casts to boolean.
|
||||
* The fix changes $cast to $casts to enable proper Laravel boolean casting.
|
||||
*/
|
||||
|
||||
use App\Models\ApplicationSetting;
|
||||
|
||||
it('casts is_static to boolean when true', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = true;
|
||||
|
||||
// Verify it's cast to boolean
|
||||
expect($setting->is_static)->toBeTrue()
|
||||
->and($setting->is_static)->toBeBool();
|
||||
});
|
||||
|
||||
it('casts is_static to boolean when false', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = false;
|
||||
|
||||
// Verify it's cast to boolean
|
||||
expect($setting->is_static)->toBeFalse()
|
||||
->and($setting->is_static)->toBeBool();
|
||||
});
|
||||
|
||||
it('casts is_static from string "1" to boolean true', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = '1';
|
||||
|
||||
// Should cast string to boolean
|
||||
expect($setting->is_static)->toBeTrue()
|
||||
->and($setting->is_static)->toBeBool();
|
||||
});
|
||||
|
||||
it('casts is_static from string "0" to boolean false', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = '0';
|
||||
|
||||
// Should cast string to boolean
|
||||
expect($setting->is_static)->toBeFalse()
|
||||
->and($setting->is_static)->toBeBool();
|
||||
});
|
||||
|
||||
it('casts is_static from integer 1 to boolean true', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = 1;
|
||||
|
||||
// Should cast integer to boolean
|
||||
expect($setting->is_static)->toBeTrue()
|
||||
->and($setting->is_static)->toBeBool();
|
||||
});
|
||||
|
||||
it('casts is_static from integer 0 to boolean false', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = 0;
|
||||
|
||||
// Should cast integer to boolean
|
||||
expect($setting->is_static)->toBeFalse()
|
||||
->and($setting->is_static)->toBeBool();
|
||||
});
|
||||
|
||||
it('has casts array property defined correctly', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
|
||||
// Verify the casts property exists and is configured
|
||||
$casts = $setting->getCasts();
|
||||
|
||||
expect($casts)->toHaveKey('is_static')
|
||||
->and($casts['is_static'])->toBe('boolean');
|
||||
});
|
||||
|
||||
it('casts all boolean fields correctly', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
|
||||
// Get all casts
|
||||
$casts = $setting->getCasts();
|
||||
|
||||
// Verify all expected boolean fields are cast
|
||||
$expectedBooleanCasts = [
|
||||
'is_static',
|
||||
'is_spa',
|
||||
'is_build_server_enabled',
|
||||
'is_preserve_repository_enabled',
|
||||
'is_container_label_escape_enabled',
|
||||
'is_container_label_readonly_enabled',
|
||||
'use_build_secrets',
|
||||
'is_auto_deploy_enabled',
|
||||
'is_force_https_enabled',
|
||||
'is_debug_enabled',
|
||||
'is_preview_deployments_enabled',
|
||||
'is_pr_deployments_public_enabled',
|
||||
'is_git_submodules_enabled',
|
||||
'is_git_lfs_enabled',
|
||||
'is_git_shallow_clone_enabled',
|
||||
];
|
||||
|
||||
foreach ($expectedBooleanCasts as $field) {
|
||||
expect($casts)->toHaveKey($field)
|
||||
->and($casts[$field])->toBe('boolean');
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue