Skip to content

Commit 2bfa464

Browse files
authored
Merge pull request #3186 from divaltor/slider-resources
feat(resources): Add number component to have better UX control over Docker resources
2 parents 8c7bc82 + d465fb4 commit 2bfa464

File tree

2 files changed

+136
-13
lines changed

2 files changed

+136
-13
lines changed

apps/dokploy/components/dashboard/application/advanced/show-resources.tsx

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import {
2121
FormLabel,
2222
FormMessage,
2323
} from "@/components/ui/form";
24-
import { Input } from "@/components/ui/input";
24+
import {
25+
createConverter,
26+
NumberInputWithSteps,
27+
} from "@/components/ui/number-input";
2528
import {
2629
Tooltip,
2730
TooltipContent,
@@ -30,6 +33,23 @@ import {
3033
} from "@/components/ui/tooltip";
3134
import { api } from "@/utils/api";
3235

36+
const CPU_STEP = 0.25;
37+
const MEMORY_STEP_MB = 256;
38+
39+
const formatNumber = (value: number, decimals = 2): string =>
40+
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
41+
42+
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
43+
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
44+
);
45+
46+
const memoryConverter = createConverter(1024 * 1024, (mb) => {
47+
if (mb <= 0) return "";
48+
return mb >= 1024
49+
? `${formatNumber(mb / 1024)} GB`
50+
: `${formatNumber(mb)} MB`;
51+
});
52+
3353
const addResourcesSchema = z.object({
3454
memoryReservation: z.string().optional(),
3555
cpuLimit: z.string().optional(),
@@ -51,6 +71,7 @@ interface Props {
5171
}
5272

5373
type AddResources = z.infer<typeof addResourcesSchema>;
74+
5475
export const ShowResources = ({ id, type }: Props) => {
5576
const queryMap = {
5677
postgres: () =>
@@ -163,16 +184,20 @@ export const ShowResources = ({ id, type }: Props) => {
163184
<TooltipContent>
164185
<p>
165186
Memory hard limit in bytes. Example: 1GB =
166-
1073741824 bytes
187+
1073741824 bytes. Use +/- buttons to adjust by
188+
256 MB.
167189
</p>
168190
</TooltipContent>
169191
</Tooltip>
170192
</TooltipProvider>
171193
</div>
172194
<FormControl>
173-
<Input
195+
<NumberInputWithSteps
196+
value={field.value}
197+
onChange={field.onChange}
174198
placeholder="1073741824 (1GB in bytes)"
175-
{...field}
199+
step={MEMORY_STEP_MB}
200+
converter={memoryConverter}
176201
/>
177202
</FormControl>
178203
<FormMessage />
@@ -198,16 +223,20 @@ export const ShowResources = ({ id, type }: Props) => {
198223
<TooltipContent>
199224
<p>
200225
Memory soft limit in bytes. Example: 256MB =
201-
268435456 bytes
226+
268435456 bytes. Use +/- buttons to adjust by 256
227+
MB.
202228
</p>
203229
</TooltipContent>
204230
</Tooltip>
205231
</TooltipProvider>
206232
</div>
207233
<FormControl>
208-
<Input
234+
<NumberInputWithSteps
235+
value={field.value}
236+
onChange={field.onChange}
209237
placeholder="268435456 (256MB in bytes)"
210-
{...field}
238+
step={MEMORY_STEP_MB}
239+
converter={memoryConverter}
211240
/>
212241
</FormControl>
213242
<FormMessage />
@@ -234,17 +263,20 @@ export const ShowResources = ({ id, type }: Props) => {
234263
<TooltipContent>
235264
<p>
236265
CPU quota in units of 10^-9 CPUs. Example: 2
237-
CPUs = 2000000000
266+
CPUs = 2000000000. Use +/- buttons to adjust by
267+
0.25 CPU.
238268
</p>
239269
</TooltipContent>
240270
</Tooltip>
241271
</TooltipProvider>
242272
</div>
243273
<FormControl>
244-
<Input
274+
<NumberInputWithSteps
275+
value={field.value}
276+
onChange={field.onChange}
245277
placeholder="2000000000 (2 CPUs)"
246-
{...field}
247-
value={field.value?.toString() || ""}
278+
step={CPU_STEP}
279+
converter={cpuConverter}
248280
/>
249281
</FormControl>
250282
<FormMessage />
@@ -271,14 +303,21 @@ export const ShowResources = ({ id, type }: Props) => {
271303
<TooltipContent>
272304
<p>
273305
CPU shares (relative weight). Example: 1 CPU =
274-
1000000000
306+
1000000000. Use +/- buttons to adjust by 0.25
307+
CPU.
275308
</p>
276309
</TooltipContent>
277310
</Tooltip>
278311
</TooltipProvider>
279312
</div>
280313
<FormControl>
281-
<Input placeholder="1000000000 (1 CPU)" {...field} />
314+
<NumberInputWithSteps
315+
value={field.value}
316+
onChange={field.onChange}
317+
placeholder="1000000000 (1 CPU)"
318+
step={CPU_STEP}
319+
converter={cpuConverter}
320+
/>
282321
</FormControl>
283322
<FormMessage />
284323
</FormItem>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { MinusIcon, PlusIcon } from "lucide-react";
2+
import { Button } from "@/components/ui/button";
3+
import { Input } from "@/components/ui/input";
4+
5+
export interface UnitConverter {
6+
toValue: (raw: string | undefined) => number;
7+
fromValue: (value: number) => string;
8+
formatDisplay: (value: number) => string;
9+
}
10+
11+
export const createConverter = (
12+
multiplier: number,
13+
formatDisplay: (value: number) => string,
14+
): UnitConverter => ({
15+
toValue: (raw) => {
16+
if (!raw) return 0;
17+
const value = Number.parseInt(raw, 10);
18+
return Number.isNaN(value) ? 0 : value / multiplier;
19+
},
20+
fromValue: (value) =>
21+
value <= 0 ? "" : String(Math.round(value * multiplier)),
22+
formatDisplay,
23+
});
24+
25+
interface NumberInputWithStepsProps {
26+
value: string | undefined;
27+
onChange: (value: string) => void;
28+
placeholder: string;
29+
step: number;
30+
converter: UnitConverter;
31+
}
32+
33+
export const NumberInputWithSteps = ({
34+
value,
35+
onChange,
36+
placeholder,
37+
step,
38+
converter,
39+
}: NumberInputWithStepsProps) => {
40+
const numericValue = converter.toValue(value);
41+
const displayValue = converter.formatDisplay(numericValue);
42+
43+
const handleIncrement = () =>
44+
onChange(converter.fromValue(numericValue + step));
45+
const handleDecrement = () =>
46+
onChange(converter.fromValue(Math.max(0, numericValue - step)));
47+
48+
return (
49+
<div className="flex flex-col gap-2">
50+
<div className="flex items-center gap-2">
51+
<Button
52+
type="button"
53+
variant="outline"
54+
size="icon"
55+
className="h-9 w-9 shrink-0"
56+
onClick={handleDecrement}
57+
disabled={numericValue <= 0}
58+
>
59+
<MinusIcon className="h-4 w-4" />
60+
</Button>
61+
<Input
62+
placeholder={placeholder}
63+
value={value || ""}
64+
onChange={(e) => onChange(e.target.value)}
65+
className="text-center"
66+
/>
67+
<Button
68+
type="button"
69+
variant="outline"
70+
size="icon"
71+
className="h-9 w-9 shrink-0"
72+
onClick={handleIncrement}
73+
>
74+
<PlusIcon className="h-4 w-4" />
75+
</Button>
76+
</div>
77+
{displayValue && (
78+
<span className="text-xs text-muted-foreground text-center">
79+
{displayValue}
80+
</span>
81+
)}
82+
</div>
83+
);
84+
};

0 commit comments

Comments
 (0)