mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
Merge 5aa5ebe0ef into 201998638a
This commit is contained in:
commit
921e1d9919
7 changed files with 529 additions and 85 deletions
|
|
@ -9,31 +9,33 @@ 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',
|
||||
'limitsCpus' => 'nullable',
|
||||
'limitsCpuset' => 'nullable',
|
||||
'limitsCpuShares' => 'nullable',
|
||||
'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',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,13 @@ class Input extends Component
|
|||
public ?string $canGate = null,
|
||||
public mixed $canResource = null,
|
||||
public bool $autoDisable = true,
|
||||
public ?string $suffix = null,
|
||||
) {
|
||||
// Handle authorization-based disabling
|
||||
if ($this->canGate && $this->canResource && $this->autoDisable) {
|
||||
$hasPermission = Gate::allows($this->canGate, $this->canResource);
|
||||
|
||||
if (! $hasPermission) {
|
||||
if (!$hasPermission) {
|
||||
$this->disabled = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +61,7 @@ class Input extends Component
|
|||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
// Use original ID with random suffix for uniqueness
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||
$this->htmlId = $this->modelBinding . '-' . $uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = (string) $this->id;
|
||||
}
|
||||
|
|
@ -69,7 +70,7 @@ class Input extends Component
|
|||
$this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
|
||||
}
|
||||
if ($this->type === 'password') {
|
||||
$this->defaultClass = $this->defaultClass.' pr-[2.8rem]';
|
||||
$this->defaultClass = $this->defaultClass . ' pr-[2.8rem]';
|
||||
}
|
||||
|
||||
// $this->label = Str::title($this->label);
|
||||
|
|
|
|||
87
app/View/Components/Forms/InputWithSelect.php
Normal file
87
app/View/Components/Forms/InputWithSelect.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components\Forms;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class InputWithSelect extends Component
|
||||
{
|
||||
public ?string $modelBinding = null;
|
||||
|
||||
public ?string $htmlId = null;
|
||||
|
||||
public ?string $combinedBinding = null;
|
||||
|
||||
public function __construct(
|
||||
public ?string $id = null,
|
||||
public ?string $name = null,
|
||||
public ?string $type = 'text',
|
||||
public ?string $value = null,
|
||||
public ?string $label = null,
|
||||
public array $options = [],
|
||||
public ?string $defaultOption = null,
|
||||
public bool $required = false,
|
||||
public bool $disabled = false,
|
||||
public bool $readonly = false,
|
||||
public ?string $helper = null,
|
||||
public ?string $placeholder = null,
|
||||
public string $defaultClass = 'input',
|
||||
public string $autocomplete = 'off',
|
||||
public ?int $minlength = null,
|
||||
public ?int $maxlength = null,
|
||||
public ?float $min = null,
|
||||
public ?float $max = null,
|
||||
public bool $autofocus = false,
|
||||
public ?string $canGate = null,
|
||||
public mixed $canResource = null,
|
||||
public bool $autoDisable = true,
|
||||
) {
|
||||
// Handle authorization-based disabling
|
||||
if ($this->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;
|
||||
}
|
||||
|
||||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
$this->combinedBinding = $this->modelBinding;
|
||||
}
|
||||
|
||||
if (is_null($this->defaultOption) && !empty($this->options)) {
|
||||
$this->defaultOption = array_key_first($this->options);
|
||||
}
|
||||
|
||||
return view('components.forms.input-with-select');
|
||||
}
|
||||
}
|
||||
167
resources/views/components/forms/input-with-select.blade.php
Normal file
167
resources/views/components/forms/input-with-select.blade.php
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
@php
|
||||
$inputId = $htmlId !== 'null' ? $htmlId . '-input' : null;
|
||||
$selectId = $htmlId !== 'null' ? $htmlId . '-select' : null;
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
/* Apply dirty styling to visible input when hidden input is dirty */
|
||||
.input-with-select-container input[type="hidden"].dirty-tracker ~ input {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5 !important;
|
||||
}
|
||||
.dark .input-with-select-container input[type="hidden"].dirty-tracker ~ input {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="w-full"
|
||||
x-data="inputWithSelect({
|
||||
defaultUnit: @js($defaultOption ?? ''),
|
||||
min: @js($min),
|
||||
max: @js($max),
|
||||
validUnits: @js(array_keys($options)),
|
||||
@if ($modelBinding !== 'null')
|
||||
entangled: @entangle($combinedBinding),
|
||||
@else
|
||||
entangled: @js($value ?? '0'),
|
||||
@endif
|
||||
})"
|
||||
x-ref="container">
|
||||
@if ($label)
|
||||
<label @if ($inputId) for="{{ $inputId }}" @endif class="flex gap-1 items-center mb-1 text-sm font-medium">
|
||||
{{ $label }}
|
||||
@if ($required)
|
||||
<x-highlighted text="*" />
|
||||
@endif
|
||||
@if ($helper)
|
||||
<x-helper :helper="$helper" />
|
||||
@endif
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<div class="flex input-with-select-container">
|
||||
{{-- Hidden input for wire:dirty tracking (binds to combinedValue which has the full value with unit) --}}
|
||||
@if ($modelBinding !== 'null')
|
||||
<input type="hidden"
|
||||
wire:model={{ $combinedBinding }}
|
||||
wire:dirty.class="dirty-tracker"
|
||||
/>
|
||||
@endif
|
||||
|
||||
{{-- Input --}}
|
||||
<input
|
||||
type="{{ $type }}"
|
||||
@if ($inputId) id="{{ $inputId }}" @endif
|
||||
x-model="value"
|
||||
@blur="commit()"
|
||||
@disabled($disabled)
|
||||
@readonly($readonly)
|
||||
placeholder="{{ $placeholder }}"
|
||||
autocomplete="{{ $autocomplete }}"
|
||||
name="{{ $name }}-input"
|
||||
@if ($min !== null) min="{{ $min }}" @endif
|
||||
@if ($max !== null) max="{{ $max }}" @endif
|
||||
minlength="{{ $minlength }}"
|
||||
maxlength="{{ $maxlength }}"
|
||||
class="{{ $defaultClass }} rounded-r-none flex-1 border-r-0"
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif
|
||||
aria-label="{{ $label }}"
|
||||
x-ref="input"
|
||||
/>
|
||||
|
||||
{{-- Select --}}
|
||||
<select
|
||||
@if ($selectId) id="{{ $selectId }}" @endif
|
||||
x-model="unit"
|
||||
@change="commit()"
|
||||
@disabled($disabled)
|
||||
name="{{ $name }}-select"
|
||||
class="select rounded-l-none w-auto min-w-[70px] border-l-0"
|
||||
aria-label="{{ $label }} unit"
|
||||
>
|
||||
@foreach($options as $key => $display)
|
||||
<option value="{{ $key }}">{{ $display }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (!$label && $helper)
|
||||
<x-helper :helper="$helper" />
|
||||
@endif
|
||||
@error($modelBinding)
|
||||
<label class="label">
|
||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let registered = false;
|
||||
|
||||
function registerInputWithSelect() {
|
||||
// Prevent duplicate registration
|
||||
if (registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
Alpine.data('inputWithSelect', ({ defaultUnit, validUnits, entangled }) => ({
|
||||
value: '',
|
||||
unit: defaultUnit,
|
||||
entangled: entangled,
|
||||
|
||||
init() {
|
||||
this.fromCombined(this.entangled);
|
||||
|
||||
// Watch for external changes from Livewire
|
||||
this.$watch('entangled', (newVal) => {
|
||||
const current = this.combined;
|
||||
if (newVal !== current) {
|
||||
this.fromCombined(newVal);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
get combined() {
|
||||
return this.value ? this.value + this.unit : '0';
|
||||
},
|
||||
|
||||
commit() {
|
||||
this.entangled = this.combined;
|
||||
},
|
||||
|
||||
fromCombined(raw) {
|
||||
if (!raw || raw === '0' || raw === 'null' || raw === null) {
|
||||
this.value = '';
|
||||
this.unit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
|
||||
const units = [...validUnits].sort((a, b) => b.length - a.length);
|
||||
for (const u of units) {
|
||||
if (raw.endsWith(u)) {
|
||||
const val = raw.slice(0, -u.length);
|
||||
if (val) {
|
||||
this.value = val;
|
||||
this.unit = u;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.value = raw;
|
||||
this.unit = defaultUnit;
|
||||
}
|
||||
}));
|
||||
|
||||
registered = true;
|
||||
}
|
||||
|
||||
// Alpine already initialized (SPA navigation) - register immediately
|
||||
if (window.Alpine) {
|
||||
registerInputWithSelect();
|
||||
}
|
||||
|
||||
// Also listen for alpine:init (initial page load)
|
||||
document.addEventListener('alpine:init', registerInputWithSelect);
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
@php
|
||||
$hasSuffix = (isset($suffix) && $suffix instanceof \Illuminate\View\ComponentSlot && $suffix->isNotEmpty()) || ($suffix ?? null);
|
||||
$inputClass = $hasSuffix ? $defaultClass . ' rounded-r-none border-r-0' : $defaultClass;
|
||||
@endphp
|
||||
|
||||
<div @class([
|
||||
'flex-1' => $isMultiline,
|
||||
'w-full' => !$isMultiline,
|
||||
|
|
@ -45,8 +50,11 @@
|
|||
|
||||
</div>
|
||||
@else
|
||||
@if ($hasSuffix)
|
||||
<div class="flex">
|
||||
@endif
|
||||
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
|
||||
{{ $attributes->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)
|
||||
<span class="flex items-center px-3 border border-l-0 rounded-r bg-coolgray-100 text-neutral-400 select-none">
|
||||
{{ $suffix }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
@if (!$label && $helper)
|
||||
<x-helper :helper="$helper" />
|
||||
|
|
|
|||
|
|
@ -1,40 +1,107 @@
|
|||
<div>
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2 ">
|
||||
<form wire:submit='submit' class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Resource Limits</h2>
|
||||
<x-forms.button canGate="update" :canResource="$resource" type='submit'>Save</x-forms.button>
|
||||
</div>
|
||||
<div class="">Limit your container resources by CPU & memory.</div>
|
||||
<h3 class="pt-4">Limit CPUs</h3>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="1.5"
|
||||
helper="0 means use all CPUs. Floating point number, like 0.002 or 1.5. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
||||
label="Number of CPUs" id="limitsCpus" />
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="0-2"
|
||||
helper="Empty means, use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
||||
label="CPU sets to use" id="limitsCpuset" />
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="1024"
|
||||
helper="More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
|
||||
label="CPU Weight" id="limitsCpuShares" />
|
||||
</div>
|
||||
<h3 class="pt-4">Limit Memory</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
|
||||
label="Soft Memory Limit" id="limitsMemoryReservation" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="0-100.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
|
||||
type="number" min="0" max="100" label="Swappiness"
|
||||
id="limitsMemorySwappiness" />
|
||||
<p>Limit your container resources by CPU & memory.</p>
|
||||
<div class="flex flex-col gap-3 pt-4">
|
||||
<h3>Limit CPUs</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" min="0" max="1024" step="0.1"
|
||||
placeholder="0"
|
||||
helper="Limit how much CPU the container can use. 0 means unlimited (use all available CPUs). Use decimal numbers like 1.5 for one and a half CPUs, or 0.5 for half a CPU.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-quota-constraint'>cpu-quota</a>."
|
||||
label="CPU Limit" id="limitsCpus">
|
||||
<x-slot:suffix>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z" />
|
||||
<path d="M9 9h6v6h-6z" />
|
||||
<path d="M9 1v3" />
|
||||
<path d="M15 1v3" />
|
||||
<path d="M9 20v3" />
|
||||
<path d="M15 20v3" />
|
||||
<path d="M20 9h3" />
|
||||
<path d="M20 14h3" />
|
||||
<path d="M1 9h3" />
|
||||
<path d="M1 14h3" />
|
||||
<path d="M12 9v6" />
|
||||
<path d="M9 12h6" />
|
||||
</svg>
|
||||
</x-slot:suffix>
|
||||
</x-forms.input>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="0"
|
||||
helper="Pin container to specific CPU threads. 0 means use all threads. Example: 0-1,4 results in using threads 0,1,4.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpuset-constraint'>cpuset</a>."
|
||||
label="CPU sets to use" id="limitsCpuset">
|
||||
<x-slot:suffix>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 4a3 3 0 0 1 6 0c0 1.657 -1.343 3 -3 3s-3 -1.343 -3 -3" />
|
||||
<path d="M12 7v13" />
|
||||
<path d="M9 20h6" />
|
||||
</svg>
|
||||
</x-slot:suffix>
|
||||
</x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" min="0" max="8192" step="64"
|
||||
placeholder="1024"
|
||||
helper="Relative CPU priority when containers compete for resources. Default: 1024 (normal). Examples: 512 = half priority, 2048 = double priority.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>cpu_shares</a>."
|
||||
label="CPU Weight" id="limitsCpuShares">
|
||||
<x-slot:suffix>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M19 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M5 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M19 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M5 7l0 10" />
|
||||
<path d="M19 7l0 10" />
|
||||
<path d="M7 5l10 0" />
|
||||
<path d="M7 19l10 0" />
|
||||
</svg>
|
||||
</x-slot:suffix>
|
||||
</x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
|
||||
label="Maximum Memory Limit" id="limitsMemory" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Examples:69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
|
||||
label="Maximum Swap Limit" id="limitsMemorySwap" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 pt-4">
|
||||
<h3>Limit Memory</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<x-forms.input-with-select canGate="update" :canResource="$resource"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
helper="Hard limit on container memory usage. The container will be killed if it exceeds this limit.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>mem_limit</a>."
|
||||
label="Memory Limit" id="limitsMemory"
|
||||
:options="['b' => 'B', 'k' => 'KiB', 'm' => 'MiB', 'g' => 'GiB']"
|
||||
defaultOption="m" />
|
||||
<x-forms.input-with-select canGate="update" :canResource="$resource"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
helper="Guaranteed memory reservation for the container. Docker attempts to ensure this amount is always available.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>mem_reservation</a>."
|
||||
label="Memory Reservation" id="limitsMemoryReservation"
|
||||
:options="['b' => 'B', 'k' => 'KiB', 'm' => 'MiB', 'g' => 'GiB']"
|
||||
defaultOption="m" />
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<x-forms.input-with-select canGate="update" :canResource="$resource"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
helper="Total limit for memory plus swap space. Combined limit for both RAM and swap usage.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>memswap_limit</a>."
|
||||
label="Maximum Swap Limit" id="limitsMemorySwap"
|
||||
:options="['b' => 'B', 'k' => 'KiB', 'm' => 'MiB', 'g' => 'GiB']"
|
||||
defaultOption="m" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="60"
|
||||
helper="Control how aggressively the kernel swaps memory. 0 = swap only when necessary, 100 = swap aggressively. Default: 60.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>mem_swappiness</a>."
|
||||
type="number" min="0" max="100" label="Swappiness"
|
||||
id="limitsMemorySwappiness" suffix="%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
77
tests/Unit/InputWithSelectComponentTest.php
Normal file
77
tests/Unit/InputWithSelectComponentTest.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
use App\View\Components\Forms\InputWithSelect;
|
||||
|
||||
it('renders with default properties', function () {
|
||||
$component = new InputWithSelect;
|
||||
|
||||
expect($component->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();
|
||||
});
|
||||
Loading…
Reference in a new issue