11import { zodResolver } from "@hookform/resolvers/zod" ;
2- import { InfoIcon } from "lucide-react" ;
2+ import { InfoIcon , Plus , Trash2 } from "lucide-react" ;
33import { useEffect } from "react" ;
4- import { useForm } from "react-hook-form" ;
4+ import { useFieldArray , useForm } from "react-hook-form" ;
55import { toast } from "sonner" ;
66import { z } from "zod" ;
77import { AlertBlock } from "@/components/shared/alert-block" ;
@@ -21,10 +21,18 @@ import {
2121 FormLabel ,
2222 FormMessage ,
2323} from "@/components/ui/form" ;
24+ import { Input } from "@/components/ui/input" ;
2425import {
2526 createConverter ,
2627 NumberInputWithSteps ,
2728} from "@/components/ui/number-input" ;
29+ import {
30+ Select ,
31+ SelectContent ,
32+ SelectItem ,
33+ SelectTrigger ,
34+ SelectValue ,
35+ } from "@/components/ui/select" ;
2836import {
2937 Tooltip ,
3038 TooltipContent ,
@@ -50,13 +58,36 @@ const memoryConverter = createConverter(1024 * 1024, (mb) => {
5058 : `${ formatNumber ( mb ) } MB` ;
5159} ) ;
5260
61+ const ulimitSchema = z . object ( {
62+ Name : z . string ( ) . min ( 1 , "Name is required" ) ,
63+ Soft : z . coerce . number ( ) . int ( ) . min ( - 1 , "Must be >= -1" ) ,
64+ Hard : z . coerce . number ( ) . int ( ) . min ( - 1 , "Must be >= -1" ) ,
65+ } ) ;
66+
5367const addResourcesSchema = z . object ( {
5468 memoryReservation : z . string ( ) . optional ( ) ,
5569 cpuLimit : z . string ( ) . optional ( ) ,
5670 memoryLimit : z . string ( ) . optional ( ) ,
5771 cpuReservation : z . string ( ) . optional ( ) ,
72+ ulimitsSwarm : z . array ( ulimitSchema ) . optional ( ) ,
5873} ) ;
5974
75+ const ULIMIT_PRESETS = [
76+ { value : "nofile" , label : "nofile (Open Files)" } ,
77+ { value : "nproc" , label : "nproc (Processes)" } ,
78+ { value : "memlock" , label : "memlock (Locked Memory)" } ,
79+ { value : "stack" , label : "stack (Stack Size)" } ,
80+ { value : "core" , label : "core (Core File Size)" } ,
81+ { value : "cpu" , label : "cpu (CPU Time)" } ,
82+ { value : "data" , label : "data (Data Segment)" } ,
83+ { value : "fsize" , label : "fsize (File Size)" } ,
84+ { value : "locks" , label : "locks (File Locks)" } ,
85+ { value : "msgqueue" , label : "msgqueue (Message Queues)" } ,
86+ { value : "nice" , label : "nice (Nice Priority)" } ,
87+ { value : "rtprio" , label : "rtprio (Real-time Priority)" } ,
88+ { value : "sigpending" , label : "sigpending (Pending Signals)" } ,
89+ ] ;
90+
6091export type ServiceType =
6192 | "postgres"
6293 | "mongo"
@@ -107,17 +138,24 @@ export const ShowResources = ({ id, type }: Props) => {
107138 cpuReservation : "" ,
108139 memoryLimit : "" ,
109140 memoryReservation : "" ,
141+ ulimitsSwarm : [ ] ,
110142 } ,
111143 resolver : zodResolver ( addResourcesSchema ) ,
112144 } ) ;
113145
146+ const { fields, append, remove } = useFieldArray ( {
147+ control : form . control ,
148+ name : "ulimitsSwarm" ,
149+ } ) ;
150+
114151 useEffect ( ( ) => {
115152 if ( data ) {
116153 form . reset ( {
117154 cpuLimit : data ?. cpuLimit || undefined ,
118155 cpuReservation : data ?. cpuReservation || undefined ,
119156 memoryLimit : data ?. memoryLimit || undefined ,
120157 memoryReservation : data ?. memoryReservation || undefined ,
158+ ulimitsSwarm : data ?. ulimitsSwarm || [ ] ,
121159 } ) ;
122160 }
123161 } , [ data , form , form . reset ] ) ;
@@ -134,6 +172,10 @@ export const ShowResources = ({ id, type }: Props) => {
134172 cpuReservation : formData . cpuReservation || null ,
135173 memoryLimit : formData . memoryLimit || null ,
136174 memoryReservation : formData . memoryReservation || null ,
175+ ulimitsSwarm :
176+ formData . ulimitsSwarm && formData . ulimitsSwarm . length > 0
177+ ? formData . ulimitsSwarm
178+ : null ,
137179 } )
138180 . then ( async ( ) => {
139181 toast . success ( "Resources Updated" ) ;
@@ -325,6 +367,145 @@ export const ShowResources = ({ id, type }: Props) => {
325367 } }
326368 />
327369 </ div >
370+
371+ { /* Ulimits Section */ }
372+ < div className = "space-y-4" >
373+ < div className = "flex items-center justify-between" >
374+ < div className = "flex items-center gap-2" >
375+ < FormLabel className = "text-base" > Ulimits</ FormLabel >
376+ < TooltipProvider >
377+ < Tooltip delayDuration = { 0 } >
378+ < TooltipTrigger >
379+ < InfoIcon className = "h-4 w-4 text-muted-foreground" />
380+ </ TooltipTrigger >
381+ < TooltipContent className = "max-w-xs" >
382+ < p >
383+ Set resource limits for the container. Each ulimit has
384+ a soft limit (warning threshold) and hard limit
385+ (maximum allowed). Use -1 for unlimited.
386+ </ p >
387+ </ TooltipContent >
388+ </ Tooltip >
389+ </ TooltipProvider >
390+ </ div >
391+ < Button
392+ type = "button"
393+ variant = "outline"
394+ size = "sm"
395+ onClick = { ( ) =>
396+ append ( { Name : "nofile" , Soft : 65535 , Hard : 65535 } )
397+ }
398+ >
399+ < Plus className = "h-4 w-4 mr-1" />
400+ Add Ulimit
401+ </ Button >
402+ </ div >
403+
404+ { fields . length > 0 && (
405+ < div className = "space-y-3" >
406+ { fields . map ( ( field , index ) => (
407+ < div
408+ key = { field . id }
409+ className = "flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
410+ >
411+ < FormField
412+ control = { form . control }
413+ name = { `ulimitsSwarm.${ index } .Name` }
414+ render = { ( { field } ) => (
415+ < FormItem className = "flex-1" >
416+ < FormLabel className = "text-xs" > Type</ FormLabel >
417+ < Select
418+ onValueChange = { field . onChange }
419+ value = { field . value }
420+ >
421+ < FormControl >
422+ < SelectTrigger >
423+ < SelectValue placeholder = "Select ulimit" />
424+ </ SelectTrigger >
425+ </ FormControl >
426+ < SelectContent >
427+ { ULIMIT_PRESETS . map ( ( preset ) => (
428+ < SelectItem
429+ key = { preset . value }
430+ value = { preset . value }
431+ >
432+ { preset . label }
433+ </ SelectItem >
434+ ) ) }
435+ </ SelectContent >
436+ </ Select >
437+ < FormMessage />
438+ </ FormItem >
439+ ) }
440+ />
441+ < FormField
442+ control = { form . control }
443+ name = { `ulimitsSwarm.${ index } .Soft` }
444+ render = { ( { field } ) => (
445+ < FormItem className = "w-32" >
446+ < FormLabel className = "text-xs" >
447+ Soft Limit
448+ </ FormLabel >
449+ < FormControl >
450+ < Input
451+ type = "number"
452+ min = { - 1 }
453+ placeholder = "65535"
454+ { ...field }
455+ onChange = { ( e ) =>
456+ field . onChange ( Number ( e . target . value ) )
457+ }
458+ />
459+ </ FormControl >
460+ < FormMessage />
461+ </ FormItem >
462+ ) }
463+ />
464+ < FormField
465+ control = { form . control }
466+ name = { `ulimitsSwarm.${ index } .Hard` }
467+ render = { ( { field } ) => (
468+ < FormItem className = "w-32" >
469+ < FormLabel className = "text-xs" >
470+ Hard Limit
471+ </ FormLabel >
472+ < FormControl >
473+ < Input
474+ type = "number"
475+ min = { - 1 }
476+ placeholder = "65535"
477+ { ...field }
478+ onChange = { ( e ) =>
479+ field . onChange ( Number ( e . target . value ) )
480+ }
481+ />
482+ </ FormControl >
483+ < FormMessage />
484+ </ FormItem >
485+ ) }
486+ />
487+ < Button
488+ type = "button"
489+ variant = "ghost"
490+ size = "icon"
491+ className = "mt-6 text-destructive hover:text-destructive"
492+ onClick = { ( ) => remove ( index ) }
493+ >
494+ < Trash2 className = "h-4 w-4" />
495+ </ Button >
496+ </ div >
497+ ) ) }
498+ </ div >
499+ ) }
500+
501+ { fields . length === 0 && (
502+ < p className = "text-sm text-muted-foreground" >
503+ No ulimits configured. Click "Add Ulimit" to set
504+ resource limits.
505+ </ p >
506+ ) }
507+ </ div >
508+
328509 < div className = "flex w-full justify-end" >
329510 < Button isLoading = { isLoading } type = "submit" >
330511 Save
0 commit comments