From ef0e33b9b987b284c23677f9ede588cd5be346f3 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 03:48:05 +0100 Subject: [PATCH 01/13] feat(ui): create InputWithSelect component and enhance resource limits page - Replace standard input fields with input-select components for Soft Memory Limit, Maximum Memory Limit, and Maximum Swap Limit. - Update helper texts to provide clearer guidance on memory settings and link to Docker documentation. - Ensure all memory inputs support unit selection (B, KiB, MiB, GiB) for better user experience. - Add unit tests for new component --- app/View/Components/Forms/InputWithSelect.php | 82 +++++++++ .../forms/input-with-select.blade.php | 169 ++++++++++++++++++ .../project/shared/resource-limits.blade.php | 30 +++- tests/Unit/InputWithSelectComponentTest.php | 77 ++++++++ 4 files changed, 349 insertions(+), 9 deletions(-) create mode 100644 app/View/Components/Forms/InputWithSelect.php create mode 100644 resources/views/components/forms/input-with-select.blade.php create mode 100644 tests/Unit/InputWithSelectComponentTest.php diff --git a/app/View/Components/Forms/InputWithSelect.php b/app/View/Components/Forms/InputWithSelect.php new file mode 100644 index 000000000..1ce139cec --- /dev/null +++ b/app/View/Components/Forms/InputWithSelect.php @@ -0,0 +1,82 @@ +canGate && $this->canResource && $this->autoDisable) { + $hasPermission = Gate::allows($this->canGate, $this->canResource); + + if (!$hasPermission) { + $this->disabled = true; + } + } + } + + public function render(): View|Closure|string + { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + + if (is_null($this->id)) { + $this->id = new Cuid2; + // Don't create wire:model binding for auto-generated IDs + $this->modelBinding = 'null'; + } + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->modelBinding && $this->modelBinding !== 'null') { + // Use original ID with random suffix for uniqueness + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->modelBinding . '-' . $uniqueSuffix; + } else { + $this->htmlId = (string) $this->id; + } + + if (is_null($this->name)) { + $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; + } + + // Set default option if not provided and options exist + if (is_null($this->defaultOption) && !empty($this->options)) { + $this->defaultOption = array_key_first($this->options); + } + + return view('components.forms.input-with-select'); + } +} diff --git a/resources/views/components/forms/input-with-select.blade.php b/resources/views/components/forms/input-with-select.blade.php new file mode 100644 index 000000000..f9b1f4c9b --- /dev/null +++ b/resources/views/components/forms/input-with-select.blade.php @@ -0,0 +1,169 @@ +@php + $inputId = $htmlId !== 'null' ? $htmlId . '-input' : null; + $selectId = $htmlId !== 'null' ? $htmlId . '-select' : null; +@endphp + +
+ @if ($label) + + @endif + +
+ {{-- Input --}} + + + {{-- Select --}} + +
+ + @if (!$label && $helper) + + @endif + @error($modelBinding) + + @enderror +
+ + diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index 99ff249e9..da1cf3722 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -20,21 +20,33 @@

Limit Memory

- +
- - + +
diff --git a/tests/Unit/InputWithSelectComponentTest.php b/tests/Unit/InputWithSelectComponentTest.php new file mode 100644 index 000000000..2d7565cb0 --- /dev/null +++ b/tests/Unit/InputWithSelectComponentTest.php @@ -0,0 +1,77 @@ +required)->toBeFalse() + ->and($component->disabled)->toBeFalse() + ->and($component->readonly)->toBeFalse() + ->and($component->defaultClass)->toBe('input') + ->and($component->type)->toBe('text') + ->and($component->options)->toBe([]); +}); + +it('uses provided id', function () { + $component = new InputWithSelect(id: 'test-input-select'); + + expect($component->id)->toBe('test-input-select'); +}); + +it('accepts options array', function () { + $options = ['b' => 'B', 'k' => 'KiB', 'm' => 'MiB', 'g' => 'GiB']; + $component = new InputWithSelect(options: $options); + + expect($component->options)->toBe($options); +}); + +it('sets default option to first option when not provided', function () { + $options = ['b' => 'B', 'k' => 'KiB', 'm' => 'MiB']; + $component = new InputWithSelect(options: $options); + + // defaultOption is set in render(), so we test the logic directly + if (is_null($component->defaultOption) && !empty($component->options)) { + $component->defaultOption = array_key_first($component->options); + } + + expect($component->defaultOption)->toBe('b'); +}); + +it('uses provided default option', function () { + $options = ['b' => 'B', 'k' => 'KiB', 'm' => 'MiB']; + $component = new InputWithSelect(options: $options, defaultOption: 'm'); + + expect($component->defaultOption)->toBe('m'); +}); + +it('accepts min and max values', function () { + $component = new InputWithSelect(min: 0, max: 100); + + expect($component->min)->toBe(0.0) + ->and($component->max)->toBe(100.0); +}); + +it('accepts type parameter', function () { + $component = new InputWithSelect(type: 'number'); + + expect($component->type)->toBe('number'); +}); + +it('accepts authorization properties', function () { + $component = new InputWithSelect( + canGate: 'update', + canResource: 'resource', + autoDisable: false + ); + + expect($component->canGate)->toBe('update') + ->and($component->canResource)->toBe('resource') + ->and($component->autoDisable)->toBeFalse(); +}); + +it('can be manually disabled', function () { + $component = new InputWithSelect(disabled: true); + + expect($component->disabled)->toBeTrue(); +}); From 60ec7484b7e228e56aa7b608b76e95b74318b464 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 04:26:39 +0100 Subject: [PATCH 02/13] feat(InputWithSelect): add combinedBinding + structuredBinding support --- app/View/Components/Forms/InputWithSelect.php | 10 +- .../forms/input-with-select.blade.php | 101 +++++++++++++----- 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/app/View/Components/Forms/InputWithSelect.php b/app/View/Components/Forms/InputWithSelect.php index 1ce139cec..ee25b2535 100644 --- a/app/View/Components/Forms/InputWithSelect.php +++ b/app/View/Components/Forms/InputWithSelect.php @@ -14,6 +14,10 @@ class InputWithSelect extends Component public ?string $htmlId = null; + public ?string $combinedBinding = null; + + public ?string $structuredBinding = null; + public function __construct( public ?string $id = null, public ?string $name = null, @@ -72,7 +76,11 @@ class InputWithSelect extends Component $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; } - // Set default option if not provided and options exist + if ($this->modelBinding && $this->modelBinding !== 'null') { + $this->combinedBinding = $this->modelBinding; + $this->structuredBinding = $this->modelBinding . 'Structured'; + } + if (is_null($this->defaultOption) && !empty($this->options)) { $this->defaultOption = array_key_first($this->options); } diff --git a/resources/views/components/forms/input-with-select.blade.php b/resources/views/components/forms/input-with-select.blade.php index f9b1f4c9b..944ce6047 100644 --- a/resources/views/components/forms/input-with-select.blade.php +++ b/resources/views/components/forms/input-with-select.blade.php @@ -8,10 +8,13 @@ defaultUnit: @js($defaultOption ?? ''), min: @js($min), max: @js($max), + validUnits: @js(array_keys($options)), @if ($modelBinding !== 'null') - combinedValue: @entangle($modelBinding), + combinedValue: @entangle($combinedBinding), + structuredValue: @entangle($structuredBinding), @else combinedValue: @js($value ?? '0'), + structuredValue: @js(['value' => $value ?? '', 'unit' => $defaultOption ?? '']), @endif })" x-ref="container"> @@ -54,7 +57,7 @@ merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) + {{ $attributes->merge(['class' => $inputClass]) }} @required($required) @readonly($readonly) @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}" @@ -55,6 +63,12 @@ @if ($htmlId !== 'null') id={{ $htmlId }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif> + @if ($hasSuffix) + + {{ $suffix }} + + + @endif @endif @if (!$label && $helper) From 1a7b02940ec5f63e383209b99867f8b79f49c6c8 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 04:39:41 +0100 Subject: [PATCH 04/13] chore: enhance tooltips in resource limits page --- .../project/shared/resource-limits.blade.php | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index da1cf3722..ee77328f8 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -8,13 +8,29 @@

Limit CPUs

+ helper="Empty means use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info cpuset." + label="CPU sets to use" id="limitsCpuset"> + + + + + + + + + + + + + + + +

Limit Memory

@@ -23,27 +39,27 @@ + id="limitsMemorySwappiness" suffix="%" />
From f8fde69e8d89fadbee72e536773a3d2df48182dd Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 04:58:11 +0100 Subject: [PATCH 05/13] refactor(limits): add icons and update texts --- .../project/shared/resource-limits.blade.php | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index ee77328f8..452b27ea3 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -7,12 +7,10 @@
Limit your container resources by CPU & memory.

Limit CPUs

- - + label="Number of CPU threads" id="limitsCpus"> @@ -26,12 +24,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Limit Memory

From 4e05e981a29fff0cf526d63ecdbaeff85148f65f Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 05:16:13 +0100 Subject: [PATCH 06/13] refactor(limits): streamline order of UI elements for user use cases + fix incorrect links and translations +make responsive --- .../project/shared/resource-limits.blade.php | 188 ++++++++++-------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index 452b27ea3..f0f534c61 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -1,95 +1,109 @@
-
-
+ +

Resource Limits

Save
-
Limit your container resources by CPU & memory.
-

Limit CPUs

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Limit Memory

-
-
- - +
+
Limit your container resources by CPU & memory.
+
+

Limit CPUs

+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
-
- - +
+

Limit Memory

+
+
+ + +
+
+ + +
+
From 8b28e2986b236914bca344c03a21da4ba527c43f Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 05:20:11 +0100 Subject: [PATCH 07/13] refactor(limits): update labels for memory fields --- .../views/livewire/project/shared/resource-limits.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index f0f534c61..7793f9cda 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -76,7 +76,7 @@ min="0" placeholder="512" helper="Hard limit on container memory usage. The container will be killed if it exceeds this limit.
More info mem_limit." - label="Maximum Memory Limit" id="limitsMemory" + label="Memory Limit" id="limitsMemory" :options="['b' => 'B', 'k' => 'KiB', 'm' => 'MiB', 'g' => 'GiB']" defaultOption="m" />
From 96a93bfe2a073080cd70514f9c8c730eca25f512 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 05:45:19 +0100 Subject: [PATCH 08/13] refactor(limits): enhance validation rules and UI for CPU limits --- app/Livewire/Project/Shared/ResourceLimits.php | 14 +++++++------- .../project/shared/resource-limits.blade.php | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index 0b3840289..e03934845 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -31,9 +31,9 @@ class ResourceLimits extends Component 'limitsMemorySwap' => 'required|string', 'limitsMemorySwappiness' => 'required|integer|min:0|max:100', 'limitsMemoryReservation' => 'required|string', - 'limitsCpus' => 'nullable', - 'limitsCpuset' => 'nullable', - 'limitsCpuShares' => 'nullable', + 'limitsCpus' => 'nullable|numeric|min:0|max:1024', + 'limitsCpuset' => 'nullable|string', + 'limitsCpuShares' => 'nullable|integer|min:0|max:8192', ]; protected $validationAttributes = [ @@ -85,19 +85,19 @@ class ResourceLimits extends Component $this->authorize('update', $this->resource); // Apply default values to properties - if (! $this->limitsMemory) { + if (!$this->limitsMemory) { $this->limitsMemory = '0'; } - if (! $this->limitsMemorySwap) { + if (!$this->limitsMemorySwap) { $this->limitsMemorySwap = '0'; } if (is_null($this->limitsMemorySwappiness)) { $this->limitsMemorySwappiness = 60; } - if (! $this->limitsMemoryReservation) { + if (!$this->limitsMemoryReservation) { $this->limitsMemoryReservation = '0'; } - if (! $this->limitsCpus) { + if (!$this->limitsCpus) { $this->limitsCpus = '0'; } if ($this->limitsCpuset === '') { diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index 7793f9cda..b88d1edee 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -10,7 +10,7 @@

Limit CPUs

- @@ -46,7 +46,7 @@ - From 5d21b9fadd52a55f8a1ccc0c62dfdfc1b027e51b Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 05:50:12 +0100 Subject: [PATCH 09/13] refactor(InputWithSelect): remove manual clamp of minmax --- .../components/forms/input-with-select.blade.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/resources/views/components/forms/input-with-select.blade.php b/resources/views/components/forms/input-with-select.blade.php index 944ce6047..d9f72143f 100644 --- a/resources/views/components/forms/input-with-select.blade.php +++ b/resources/views/components/forms/input-with-select.blade.php @@ -156,16 +156,6 @@ document.addEventListener('alpine:init', () => { return { value: combined, unit: config.defaultUnit }; }, - validateAndClamp() { - const numValue = parseFloat(this.inputValue); - if (isNaN(numValue)) return; - if (config.min !== null && numValue < config.min) { - this.inputValue = String(config.min); - } else if (config.max !== null && numValue > config.max) { - this.inputValue = String(config.max); - } - }, - toStructured() { return { value: this.inputValue || '', @@ -183,14 +173,11 @@ document.addEventListener('alpine:init', () => { }, updateStructured() { - this.validateAndClamp(); this.structuredValue = this.toStructured(); this.updateCombined(); }, handleInputChange() { - this.validateAndClamp(); - if (this.pendingSync) { clearTimeout(this.pendingSync); } From 4e643c3226e8ca8353e7a78cfb938d0a29de1c80 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 06:06:20 +0100 Subject: [PATCH 10/13] refactor(ResourceLimits): make all params optional and use defaults in DB; adjust UI placeholders for clarity --- .../Project/Shared/ResourceLimits.php | 123 +++++++++++------- .../project/shared/resource-limits.blade.php | 10 +- 2 files changed, 82 insertions(+), 51 deletions(-) diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index e03934845..e1e89c140 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -9,28 +9,30 @@ class ResourceLimits extends Component { use AuthorizesRequests; - public $resource; + // Default values for resource limits + private const DEFAULT_CPU_LIMIT = 0.0; + private const DEFAULT_CPU_SET = '0'; + private const DEFAULT_CPU_SHARES = 1024; + private const DEFAULT_MEMORY_SWAPPINESS = 60; + private const DEFAULT_MEMORY_LIMIT = '0'; + private const DEFAULT_MEMORY_SWAP = '0'; + private const DEFAULT_MEMORY_RESERVATION = '0'; - // Explicit properties - public ?string $limitsCpus = null; + public mixed $resource; + public ?float $limitsCpus = null; public ?string $limitsCpuset = null; - public ?int $limitsCpuShares = null; - - public string $limitsMemory; - - public string $limitsMemorySwap; - - public int $limitsMemorySwappiness; - - public string $limitsMemoryReservation; + public ?string $limitsMemory = null; + public ?string $limitsMemorySwap = null; + public ?int $limitsMemorySwappiness = null; + public ?string $limitsMemoryReservation = null; protected $rules = [ - 'limitsMemory' => 'required|string', - 'limitsMemorySwap' => 'required|string', - 'limitsMemorySwappiness' => 'required|integer|min:0|max:100', - 'limitsMemoryReservation' => 'required|string', + 'limitsMemory' => 'nullable|string', + 'limitsMemorySwap' => 'nullable|string', + 'limitsMemorySwappiness' => 'nullable|integer|min:0|max:100', + 'limitsMemoryReservation' => 'nullable|string', 'limitsCpus' => 'nullable|numeric|min:0|max:1024', 'limitsCpuset' => 'nullable|string', 'limitsCpuShares' => 'nullable|integer|min:0|max:8192', @@ -51,7 +53,7 @@ class ResourceLimits extends Component * * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. */ - private function syncData(bool $toModel = false): void + private function syncData(bool $toModel): void { if ($toModel) { // Sync TO model (before save) @@ -62,58 +64,87 @@ class ResourceLimits extends Component $this->resource->limits_memory_swap = $this->limitsMemorySwap; $this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness; $this->resource->limits_memory_reservation = $this->limitsMemoryReservation; - } else { - // Sync FROM model (on load/refresh) - $this->limitsCpus = $this->resource->limits_cpus; - $this->limitsCpuset = $this->resource->limits_cpuset; - $this->limitsCpuShares = $this->resource->limits_cpu_shares; - $this->limitsMemory = $this->resource->limits_memory; - $this->limitsMemorySwap = $this->resource->limits_memory_swap; - $this->limitsMemorySwappiness = $this->resource->limits_memory_swappiness; - $this->limitsMemoryReservation = $this->resource->limits_memory_reservation; + + return; + } + + // Sync FROM model (on load/refresh) + $this->limitsCpus = $this->resource->limits_cpus; + $this->limitsCpuset = $this->resource->limits_cpuset; + $this->limitsCpuShares = $this->resource->limits_cpu_shares; + $this->limitsMemory = $this->resource->limits_memory; + $this->limitsMemorySwap = $this->resource->limits_memory_swap; + $this->limitsMemorySwappiness = $this->resource->limits_memory_swappiness; + $this->limitsMemoryReservation = $this->resource->limits_memory_reservation; + + // Convert default values to null so UI shows placeholders instead of defaults + if ($this->limitsCpus === self::DEFAULT_CPU_LIMIT) { + $this->limitsCpus = null; + } + if ($this->limitsCpuset === self::DEFAULT_CPU_SET) { + $this->limitsCpuset = null; + } + if ($this->limitsCpuShares === self::DEFAULT_CPU_SHARES) { + $this->limitsCpuShares = null; + } + if ($this->limitsMemorySwappiness === self::DEFAULT_MEMORY_SWAPPINESS) { + $this->limitsMemorySwappiness = null; + } + if ($this->limitsMemory === self::DEFAULT_MEMORY_LIMIT) { + $this->limitsMemory = null; + } + if ($this->limitsMemorySwap === self::DEFAULT_MEMORY_SWAP) { + $this->limitsMemorySwap = null; + } + if ($this->limitsMemoryReservation === self::DEFAULT_MEMORY_RESERVATION) { + $this->limitsMemoryReservation = null; } } - public function mount() + public function mount(): void { - $this->syncData(false); + $this->syncData(toModel: false); } - public function submit() + public function submit(): void { try { $this->authorize('update', $this->resource); - // Apply default values to properties - if (!$this->limitsMemory) { - $this->limitsMemory = '0'; + // Apply defaults for empty fields + if (empty($this->limitsMemory)) { + $this->limitsMemory = self::DEFAULT_MEMORY_LIMIT; } - if (!$this->limitsMemorySwap) { - $this->limitsMemorySwap = '0'; + if (empty($this->limitsMemorySwap)) { + $this->limitsMemorySwap = self::DEFAULT_MEMORY_SWAP; } - if (is_null($this->limitsMemorySwappiness)) { - $this->limitsMemorySwappiness = 60; + if (empty($this->limitsMemoryReservation)) { + $this->limitsMemoryReservation = self::DEFAULT_MEMORY_RESERVATION; } - if (!$this->limitsMemoryReservation) { - $this->limitsMemoryReservation = '0'; + if ($this->limitsCpus === null) { + $this->limitsCpus = self::DEFAULT_CPU_LIMIT; } - if (!$this->limitsCpus) { - $this->limitsCpus = '0'; + if (empty($this->limitsCpuset)) { + $this->limitsCpuset = self::DEFAULT_CPU_SET; } - if ($this->limitsCpuset === '') { - $this->limitsCpuset = null; + if ($this->limitsCpuShares === null) { + $this->limitsCpuShares = self::DEFAULT_CPU_SHARES; } - if (is_null($this->limitsCpuShares)) { - $this->limitsCpuShares = 1024; + if ($this->limitsMemorySwappiness === null) { + $this->limitsMemorySwappiness = self::DEFAULT_MEMORY_SWAPPINESS; } $this->validate(); - $this->syncData(true); + $this->syncData(toModel: true); $this->resource->save(); + + // Reload from model to convert defaults back to null for placeholder display + $this->syncData(toModel: false); + $this->dispatch('success', 'Resource limits updated.'); } catch (\Throwable $e) { - return handleError($e, $this); + handleError($e, $this); } } } diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index b88d1edee..01707218b 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -11,7 +11,7 @@
@@ -34,7 +34,7 @@
- @@ -74,7 +74,7 @@ Date: Sun, 11 Jan 2026 06:18:28 +0100 Subject: [PATCH 11/13] feat(InputWithSelect): add dirty tracking styling and enhance input handling --- .../forms/input-with-select.blade.php | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/resources/views/components/forms/input-with-select.blade.php b/resources/views/components/forms/input-with-select.blade.php index d9f72143f..5a9e10d4b 100644 --- a/resources/views/components/forms/input-with-select.blade.php +++ b/resources/views/components/forms/input-with-select.blade.php @@ -3,6 +3,16 @@ $selectId = $htmlId !== 'null' ? $htmlId . '-select' : null; @endphp + +
+
+ {{-- Hidden input for wire:dirty tracking (binds to combinedValue which has the full value with unit) --}} + @if ($modelBinding !== 'null') + + @endif + {{-- Input --}} { parseCombinedValue(combined) { // Parse combinedValue (e.g., "512m") into structured format // Only matches if suffix is a valid unit to avoid footguns - if (!combined || combined === '0') { + if (!combined || combined === '0' || combined === 'null' || combined === null) { return { value: '', unit: config.defaultUnit }; } From 67168ceb1e4fd6c3d85a303f125127411b4416fd Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 12:25:52 +0100 Subject: [PATCH 12/13] refactor(ResourceLimits): revert flex layout for more control --- .../project/shared/resource-limits.blade.php | 192 +++++++++--------- 1 file changed, 95 insertions(+), 97 deletions(-) diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index 01707218b..2eddcf646 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -4,105 +4,103 @@

Resource Limits

Save
-
-
Limit your container resources by CPU & memory.
-
-

Limit CPUs

-
-
- - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
+

Limit your container resources by CPU & memory.

+
+

Limit CPUs

+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
-
-

Limit Memory

-
-
- - -
-
- - -
+
+
+

Limit Memory

+
+
+ + +
+
+ +
From 5aa5ebe0ef46a92ae510403950b3e91905e2128c Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 11 Jan 2026 18:28:40 +0100 Subject: [PATCH 13/13] fix: input-with-select Alpine timing and simplify state sync Fix component registration on SPA navigation and remove unnecessary structuredValue binding. Use modern Alpine pattern with local state and explicit commits. --- app/View/Components/Forms/InputWithSelect.php | 3 - .../forms/input-with-select.blade.php | 214 +++++++----------- 2 files changed, 78 insertions(+), 139 deletions(-) diff --git a/app/View/Components/Forms/InputWithSelect.php b/app/View/Components/Forms/InputWithSelect.php index ee25b2535..0cc03b65b 100644 --- a/app/View/Components/Forms/InputWithSelect.php +++ b/app/View/Components/Forms/InputWithSelect.php @@ -16,8 +16,6 @@ class InputWithSelect extends Component public ?string $combinedBinding = null; - public ?string $structuredBinding = null; - public function __construct( public ?string $id = null, public ?string $name = null, @@ -78,7 +76,6 @@ class InputWithSelect extends Component if ($this->modelBinding && $this->modelBinding !== 'null') { $this->combinedBinding = $this->modelBinding; - $this->structuredBinding = $this->modelBinding . 'Structured'; } if (is_null($this->defaultOption) && !empty($this->options)) { diff --git a/resources/views/components/forms/input-with-select.blade.php b/resources/views/components/forms/input-with-select.blade.php index 5a9e10d4b..51784d4c2 100644 --- a/resources/views/components/forms/input-with-select.blade.php +++ b/resources/views/components/forms/input-with-select.blade.php @@ -13,18 +13,16 @@ } -
@@ -43,19 +41,18 @@
{{-- Hidden input for wire:dirty tracking (binds to combinedValue which has the full value with unit) --}} @if ($modelBinding !== 'null') - @endif - + {{-- Input --}} - {{-- Select --}} -