mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-11 17:05:25 +02:00
530 lines
15 KiB
TypeScript
530 lines
15 KiB
TypeScript
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
import { InfoIcon, Plus, Trash2 } from "lucide-react";
|
|
import { useEffect } from "react";
|
|
import { useFieldArray, useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import { AlertBlock } from "@/components/shared/alert-block";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
createConverter,
|
|
NumberInputWithSteps,
|
|
} from "@/components/ui/number-input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { api } from "@/utils/api";
|
|
|
|
const CPU_STEP = 0.25;
|
|
const MEMORY_STEP_MB = 256;
|
|
|
|
const formatNumber = (value: number, decimals = 2): string =>
|
|
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
|
|
|
|
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
|
|
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
|
|
);
|
|
|
|
const memoryConverter = createConverter(1024 * 1024, (mb) => {
|
|
if (mb <= 0) return "";
|
|
return mb >= 1024
|
|
? `${formatNumber(mb / 1024)} GB`
|
|
: `${formatNumber(mb)} MB`;
|
|
});
|
|
|
|
const ulimitSchema = z.object({
|
|
Name: z.string().min(1, "Name is required"),
|
|
Soft: z.coerce.number().int().min(-1, "Must be >= -1"),
|
|
Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
|
|
});
|
|
|
|
const addResourcesSchema = z.object({
|
|
memoryReservation: z.string().optional(),
|
|
cpuLimit: z.string().optional(),
|
|
memoryLimit: z.string().optional(),
|
|
cpuReservation: z.string().optional(),
|
|
ulimitsSwarm: z.array(ulimitSchema).optional(),
|
|
});
|
|
|
|
const ULIMIT_PRESETS = [
|
|
{ value: "nofile", label: "nofile (Open Files)" },
|
|
{ value: "nproc", label: "nproc (Processes)" },
|
|
{ value: "memlock", label: "memlock (Locked Memory)" },
|
|
{ value: "stack", label: "stack (Stack Size)" },
|
|
{ value: "core", label: "core (Core File Size)" },
|
|
{ value: "cpu", label: "cpu (CPU Time)" },
|
|
{ value: "data", label: "data (Data Segment)" },
|
|
{ value: "fsize", label: "fsize (File Size)" },
|
|
{ value: "locks", label: "locks (File Locks)" },
|
|
{ value: "msgqueue", label: "msgqueue (Message Queues)" },
|
|
{ value: "nice", label: "nice (Nice Priority)" },
|
|
{ value: "rtprio", label: "rtprio (Real-time Priority)" },
|
|
{ value: "sigpending", label: "sigpending (Pending Signals)" },
|
|
];
|
|
|
|
export type ServiceType =
|
|
| "postgres"
|
|
| "mongo"
|
|
| "redis"
|
|
| "mysql"
|
|
| "mariadb"
|
|
| "application";
|
|
|
|
interface Props {
|
|
id: string;
|
|
type: ServiceType | "application";
|
|
}
|
|
|
|
type AddResources = z.infer<typeof addResourcesSchema>;
|
|
|
|
export const ShowResources = ({ id, type }: Props) => {
|
|
const queryMap = {
|
|
postgres: () =>
|
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
mariadb: () =>
|
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
application: () =>
|
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
};
|
|
const { data, refetch } = queryMap[type]
|
|
? queryMap[type]()
|
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
|
|
|
const mutationMap = {
|
|
postgres: () => api.postgres.update.useMutation(),
|
|
redis: () => api.redis.update.useMutation(),
|
|
mysql: () => api.mysql.update.useMutation(),
|
|
mariadb: () => api.mariadb.update.useMutation(),
|
|
application: () => api.application.update.useMutation(),
|
|
mongo: () => api.mongo.update.useMutation(),
|
|
};
|
|
|
|
const { mutateAsync, isPending } = mutationMap[type]
|
|
? mutationMap[type]()
|
|
: api.mongo.update.useMutation();
|
|
|
|
const form = useForm({
|
|
defaultValues: {
|
|
cpuLimit: "",
|
|
cpuReservation: "",
|
|
memoryLimit: "",
|
|
memoryReservation: "",
|
|
ulimitsSwarm: [],
|
|
},
|
|
resolver: zodResolver(addResourcesSchema),
|
|
});
|
|
|
|
const { fields, append, remove } = useFieldArray({
|
|
control: form.control,
|
|
name: "ulimitsSwarm",
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
form.reset({
|
|
cpuLimit: data?.cpuLimit || undefined,
|
|
cpuReservation: data?.cpuReservation || undefined,
|
|
memoryLimit: data?.memoryLimit || undefined,
|
|
memoryReservation: data?.memoryReservation || undefined,
|
|
ulimitsSwarm: data?.ulimitsSwarm || [],
|
|
});
|
|
}
|
|
}, [data, form, form.reset]);
|
|
|
|
const onSubmit = async (formData: AddResources) => {
|
|
await mutateAsync({
|
|
mongoId: id || "",
|
|
postgresId: id || "",
|
|
redisId: id || "",
|
|
mysqlId: id || "",
|
|
mariadbId: id || "",
|
|
applicationId: id || "",
|
|
cpuLimit: formData.cpuLimit || null,
|
|
cpuReservation: formData.cpuReservation || null,
|
|
memoryLimit: formData.memoryLimit || null,
|
|
memoryReservation: formData.memoryReservation || null,
|
|
ulimitsSwarm:
|
|
formData.ulimitsSwarm && formData.ulimitsSwarm.length > 0
|
|
? formData.ulimitsSwarm
|
|
: null,
|
|
})
|
|
.then(async () => {
|
|
toast.success("Resources Updated");
|
|
await refetch();
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error updating the resources");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Card className="bg-background">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl">Resources</CardTitle>
|
|
<CardDescription>
|
|
If you want to decrease or increase the resources to a specific.
|
|
application or database
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-4">
|
|
<AlertBlock type="info">
|
|
Please remember to click Redeploy after modify the resources to apply
|
|
the changes.
|
|
</AlertBlock>
|
|
<Form {...form}>
|
|
<form
|
|
id="hook-form"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid w-full gap-8 "
|
|
>
|
|
<div className="grid w-full md:grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="memoryLimit"
|
|
render={({ field }) => {
|
|
return (
|
|
<FormItem>
|
|
<div
|
|
className="flex items-center gap-2"
|
|
onClick={(e) => e.preventDefault()}
|
|
>
|
|
<FormLabel>Memory Limit</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip delayDuration={0}>
|
|
<TooltipTrigger>
|
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
Memory hard limit in bytes. Example: 1GB =
|
|
1073741824 bytes. Use +/- buttons to adjust by
|
|
256 MB.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<FormControl>
|
|
<NumberInputWithSteps
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
placeholder="1073741824 (1GB in bytes)"
|
|
step={MEMORY_STEP_MB}
|
|
converter={memoryConverter}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="memoryReservation"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<div
|
|
className="flex items-center gap-2"
|
|
onClick={(e) => e.preventDefault()}
|
|
>
|
|
<FormLabel>Memory Reservation</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip delayDuration={0}>
|
|
<TooltipTrigger>
|
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
Memory soft limit in bytes. Example: 256MB =
|
|
268435456 bytes. Use +/- buttons to adjust by 256
|
|
MB.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<FormControl>
|
|
<NumberInputWithSteps
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
placeholder="268435456 (256MB in bytes)"
|
|
step={MEMORY_STEP_MB}
|
|
converter={memoryConverter}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="cpuLimit"
|
|
render={({ field }) => {
|
|
return (
|
|
<FormItem>
|
|
<div
|
|
className="flex items-center gap-2"
|
|
onClick={(e) => e.preventDefault()}
|
|
>
|
|
<FormLabel>CPU Limit</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip delayDuration={0}>
|
|
<TooltipTrigger>
|
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
CPU quota in units of 10^-9 CPUs. Example: 2
|
|
CPUs = 2000000000. Use +/- buttons to adjust by
|
|
0.25 CPU.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<FormControl>
|
|
<NumberInputWithSteps
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
placeholder="2000000000 (2 CPUs)"
|
|
step={CPU_STEP}
|
|
converter={cpuConverter}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="cpuReservation"
|
|
render={({ field }) => {
|
|
return (
|
|
<FormItem>
|
|
<div
|
|
className="flex items-center gap-2"
|
|
onClick={(e) => e.preventDefault()}
|
|
>
|
|
<FormLabel>CPU Reservation</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip delayDuration={0}>
|
|
<TooltipTrigger>
|
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
CPU shares (relative weight). Example: 1 CPU =
|
|
1000000000. Use +/- buttons to adjust by 0.25
|
|
CPU.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<FormControl>
|
|
<NumberInputWithSteps
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
placeholder="1000000000 (1 CPU)"
|
|
step={CPU_STEP}
|
|
converter={cpuConverter}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Ulimits Section */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<FormLabel className="text-base">Ulimits</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip delayDuration={0}>
|
|
<TooltipTrigger>
|
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
</TooltipTrigger>
|
|
<TooltipContent className="max-w-xs">
|
|
<p>
|
|
Set resource limits for the container. Each ulimit has
|
|
a soft limit (warning threshold) and hard limit
|
|
(maximum allowed). Use -1 for unlimited.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() =>
|
|
append({ Name: "nofile", Soft: 65535, Hard: 65535 })
|
|
}
|
|
>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
Add Ulimit
|
|
</Button>
|
|
</div>
|
|
|
|
{fields.length > 0 && (
|
|
<div className="space-y-3">
|
|
{fields.map((field, index) => (
|
|
<div
|
|
key={field.id}
|
|
className="flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name={`ulimitsSwarm.${index}.Name`}
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel className="text-xs">Type</FormLabel>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
value={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select ulimit" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{ULIMIT_PRESETS.map((preset) => (
|
|
<SelectItem
|
|
key={preset.value}
|
|
value={preset.value}
|
|
>
|
|
{preset.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name={`ulimitsSwarm.${index}.Soft`}
|
|
render={({ field }) => (
|
|
<FormItem className="w-32">
|
|
<FormLabel className="text-xs">
|
|
Soft Limit
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
min={-1}
|
|
placeholder="65535"
|
|
{...field}
|
|
value={
|
|
typeof field.value === "number"
|
|
? field.value
|
|
: ""
|
|
}
|
|
onChange={(e) =>
|
|
field.onChange(Number(e.target.value))
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name={`ulimitsSwarm.${index}.Hard`}
|
|
render={({ field }) => (
|
|
<FormItem className="w-32">
|
|
<FormLabel className="text-xs">
|
|
Hard Limit
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
min={-1}
|
|
placeholder="65535"
|
|
{...field}
|
|
value={
|
|
typeof field.value === "number"
|
|
? field.value
|
|
: ""
|
|
}
|
|
onChange={(e) =>
|
|
field.onChange(Number(e.target.value))
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="mt-6 text-destructive hover:text-destructive"
|
|
onClick={() => remove(index)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{fields.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">
|
|
No ulimits configured. Click "Add Ulimit" to set
|
|
resource limits.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex w-full justify-end">
|
|
<Button isLoading={isPending} type="submit">
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|