mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Enhance schedule management with new fields and improved components
- Introduced new fields in the schedule schema: `serviceName`, `scheduleType`, and `script`, allowing for more flexible schedule configurations. - Updated the `HandleSchedules` component to incorporate the new fields, enhancing user input options for schedule creation and updates. - Refactored the `ShowSchedules` component to support the new `scheduleType` and display relevant information based on the selected type. - Improved API handling for schedule creation and updates to accommodate the new fields, ensuring proper validation and error handling. - Added a new `ShowSchedulesModal` component for better integration of schedule viewing in server settings, enhancing user experience.
This commit is contained in:
@@ -13,7 +13,15 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Clock, Terminal, Info, PlusCircle, PenBoxIcon } from "lucide-react";
|
import {
|
||||||
|
Clock,
|
||||||
|
Terminal,
|
||||||
|
Info,
|
||||||
|
PlusCircle,
|
||||||
|
PenBoxIcon,
|
||||||
|
RefreshCw,
|
||||||
|
DatabaseZap,
|
||||||
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -37,7 +45,10 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import type { CacheType } from "../../compose/domains/add-domain";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
const commonCronExpressions = [
|
const commonCronExpressions = [
|
||||||
{ label: "Every minute", value: "* * * * *" },
|
{ label: "Every minute", value: "* * * * *" },
|
||||||
{ label: "Every hour", value: "0 * * * *" },
|
{ label: "Every hour", value: "0 * * * *" },
|
||||||
@@ -48,21 +59,66 @@ const commonCronExpressions = [
|
|||||||
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z
|
||||||
name: z.string().min(1, "Name is required"),
|
.object({
|
||||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
shellType: z.enum(["bash", "sh"]).default("bash"),
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
command: z.string().min(1, "Command is required"),
|
shellType: z.enum(["bash", "sh"]).default("bash"),
|
||||||
enabled: z.boolean().default(true),
|
command: z.string(),
|
||||||
});
|
enabled: z.boolean().default(true),
|
||||||
|
serviceName: z.string(),
|
||||||
|
scheduleType: z.enum([
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server",
|
||||||
|
]),
|
||||||
|
script: z.string(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.scheduleType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(data.scheduleType === "dokploy-server" ||
|
||||||
|
data.scheduleType === "server") &&
|
||||||
|
!data.script
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Script is required",
|
||||||
|
path: ["script"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(data.scheduleType === "application" ||
|
||||||
|
data.scheduleType === "compose") &&
|
||||||
|
!data.command
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Command is required",
|
||||||
|
path: ["command"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId?: string;
|
id?: string;
|
||||||
scheduleId?: string;
|
scheduleId?: string;
|
||||||
|
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -72,14 +128,36 @@ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
|||||||
shellType: "bash",
|
shellType: "bash",
|
||||||
command: "",
|
command: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
serviceName: "",
|
||||||
|
scheduleType: scheduleType || "application",
|
||||||
|
script: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scheduleTypeForm = form.watch("scheduleType");
|
||||||
|
|
||||||
const { data: schedule } = api.schedule.one.useQuery(
|
const { data: schedule } = api.schedule.one.useQuery(
|
||||||
{ scheduleId: scheduleId || "" },
|
{ scheduleId: scheduleId || "" },
|
||||||
{ enabled: !!scheduleId },
|
{ enabled: !!scheduleId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id || "",
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: !!id && scheduleType === "compose",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scheduleId && schedule) {
|
if (scheduleId && schedule) {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -88,6 +166,9 @@ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
|||||||
shellType: schedule.shellType,
|
shellType: schedule.shellType,
|
||||||
command: schedule.command,
|
command: schedule.command,
|
||||||
enabled: schedule.enabled,
|
enabled: schedule.enabled,
|
||||||
|
serviceName: schedule.serviceName || "",
|
||||||
|
scheduleType: schedule.scheduleType,
|
||||||
|
script: schedule.script || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, schedule, scheduleId]);
|
}, [form, schedule, scheduleId]);
|
||||||
@@ -97,18 +178,32 @@ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
|||||||
: api.schedule.create.useMutation();
|
: api.schedule.create.useMutation();
|
||||||
|
|
||||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
if (!applicationId && !scheduleId) return;
|
if (!id && !scheduleId) return;
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...values,
|
...values,
|
||||||
scheduleId: scheduleId || "",
|
scheduleId: scheduleId || "",
|
||||||
applicationId: applicationId || "",
|
...(scheduleType === "application" && {
|
||||||
|
applicationId: id || "",
|
||||||
|
}),
|
||||||
|
...(scheduleType === "compose" && {
|
||||||
|
composeId: id || "",
|
||||||
|
}),
|
||||||
|
...(scheduleType === "server" && {
|
||||||
|
serverId: id || "",
|
||||||
|
}),
|
||||||
|
...(scheduleType === "dokploy-server" && {
|
||||||
|
userId: id || "",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(
|
||||||
`Schedule ${scheduleId ? "updated" : "created"} successfully`,
|
`Schedule ${scheduleId ? "updated" : "created"} successfully`,
|
||||||
);
|
);
|
||||||
utils.schedule.list.invalidate({ applicationId });
|
utils.schedule.list.invalidate({
|
||||||
|
id,
|
||||||
|
scheduleType,
|
||||||
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -136,12 +231,130 @@ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"max-h-screen overflow-y-auto",
|
||||||
|
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
||||||
|
? "max-h-[95vh] sm:max-w-2xl"
|
||||||
|
: " sm:max-w-lg",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
{scheduleTypeForm === "compose" && (
|
||||||
|
<div className="flex flex-col w-full gap-4">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this compose,
|
||||||
|
it will read the services from the last
|
||||||
|
deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -222,59 +435,86 @@ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{(scheduleTypeForm === "application" ||
|
||||||
control={form.control}
|
scheduleTypeForm === "compose") && (
|
||||||
name="shellType"
|
<>
|
||||||
render={({ field }) => (
|
<FormField
|
||||||
<FormItem>
|
control={form.control}
|
||||||
<FormLabel className="flex items-center gap-2">
|
name="shellType"
|
||||||
<Terminal className="w-4 h-4" />
|
render={({ field }) => (
|
||||||
Shell Type
|
<FormItem>
|
||||||
</FormLabel>
|
<FormLabel className="flex items-center gap-2">
|
||||||
<Select
|
<Terminal className="w-4 h-4" />
|
||||||
onValueChange={field.onChange}
|
Shell Type
|
||||||
defaultValue={field.value}
|
</FormLabel>
|
||||||
>
|
<Select
|
||||||
<FormControl>
|
onValueChange={field.onChange}
|
||||||
<SelectTrigger>
|
defaultValue={field.value}
|
||||||
<SelectValue placeholder="Select shell type" />
|
>
|
||||||
</SelectTrigger>
|
<FormControl>
|
||||||
</FormControl>
|
<SelectTrigger>
|
||||||
<SelectContent>
|
<SelectValue placeholder="Select shell type" />
|
||||||
<SelectItem value="bash">Bash</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="sh">Sh</SelectItem>
|
</FormControl>
|
||||||
</SelectContent>
|
<SelectContent>
|
||||||
</Select>
|
<SelectItem value="bash">Bash</SelectItem>
|
||||||
<FormDescription>
|
<SelectItem value="sh">Sh</SelectItem>
|
||||||
Choose the shell to execute your command
|
</SelectContent>
|
||||||
</FormDescription>
|
</Select>
|
||||||
<FormMessage />
|
<FormDescription>
|
||||||
</FormItem>
|
Choose the shell to execute your command
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="command"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
Command
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="npm run backup" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The command to execute in your container
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
{(scheduleTypeForm === "dokploy-server" ||
|
||||||
control={form.control}
|
scheduleTypeForm === "server") && (
|
||||||
name="command"
|
<FormField
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
name="script"
|
||||||
<FormLabel className="flex items-center gap-2">
|
render={({ field }) => (
|
||||||
<Terminal className="w-4 h-4" />
|
<FormItem>
|
||||||
Command
|
<FormLabel>Script</FormLabel>
|
||||||
</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<CodeEditor
|
||||||
placeholder="docker exec my-container npm run backup"
|
language="shell"
|
||||||
{...field}
|
placeholder={`# This is a comment
|
||||||
/>
|
echo "Hello, world!"
|
||||||
</FormControl>
|
`}
|
||||||
<FormDescription>
|
className="h-96 font-mono"
|
||||||
The command to execute in your container
|
{...field}
|
||||||
</FormDescription>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -292,15 +532,8 @@ export const HandleSchedules = ({ applicationId, scheduleId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" disabled={isLoading} className="w-full">
|
<Button type="submit" isLoading={isLoading} className="w-full">
|
||||||
{isLoading ? (
|
{scheduleId ? "Update" : "Create"} Schedule
|
||||||
<>
|
|
||||||
<Clock className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
{scheduleId ? "Updating..." : "Creating..."}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>{scheduleId ? "Update" : "Create"} Schedule</>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -28,14 +28,25 @@ import {
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowSchedules = ({ applicationId }: Props) => {
|
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||||
const { data: schedules, isLoading: isLoadingSchedules } =
|
const {
|
||||||
api.schedule.list.useQuery({
|
data: schedules,
|
||||||
applicationId,
|
isLoading: isLoadingSchedules,
|
||||||
});
|
refetch: refetchSchedules,
|
||||||
|
} = api.schedule.list.useQuery(
|
||||||
|
{
|
||||||
|
id: id || "",
|
||||||
|
scheduleType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
||||||
@@ -45,7 +56,7 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
api.schedule.runManually.useMutation();
|
api.schedule.runManually.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border px-4 shadow-none bg-transparent">
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
<CardHeader className="px-0">
|
<CardHeader className="px-0">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -58,13 +69,13 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{schedules && schedules.length > 0 && (
|
{schedules && schedules.length > 0 && (
|
||||||
<HandleSchedules applicationId={applicationId} />
|
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-0">
|
<CardContent className="px-0">
|
||||||
{isLoadingSchedules ? (
|
{isLoadingSchedules ? (
|
||||||
<div className="flex gap-4 min-h-[35vh] w-full items-center justify-center text-center mx-auto">
|
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto">
|
||||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
<span className="text-sm text-muted-foreground/70">
|
<span className="text-sm text-muted-foreground/70">
|
||||||
Loading scheduled tasks...
|
Loading scheduled tasks...
|
||||||
@@ -73,7 +84,10 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
) : schedules && schedules.length > 0 ? (
|
) : schedules && schedules.length > 0 ? (
|
||||||
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
|
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
|
||||||
{schedules.map((schedule) => {
|
{schedules.map((schedule) => {
|
||||||
const application = schedule.application;
|
const serverId =
|
||||||
|
schedule.serverId ||
|
||||||
|
schedule.application?.serverId ||
|
||||||
|
schedule.compose?.serverId;
|
||||||
const deployments = schedule.deployments;
|
const deployments = schedule.deployments;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -101,31 +115,38 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="font-mono text-[10px] bg-transparent"
|
className="font-mono text-[10px] bg-transparent"
|
||||||
>
|
>
|
||||||
{schedule.cronExpression}
|
Cron: {schedule.cronExpression}
|
||||||
</Badge>
|
|
||||||
<span className="text-xs text-muted-foreground/50">
|
|
||||||
•
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="font-mono text-[10px] bg-transparent"
|
|
||||||
>
|
|
||||||
{schedule.shellType}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{schedule.scheduleType !== "server" &&
|
||||||
|
schedule.scheduleType !== "dokploy-server" && (
|
||||||
|
<>
|
||||||
|
<span className="text-xs text-muted-foreground/50">
|
||||||
|
•
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-[10px] bg-transparent"
|
||||||
|
>
|
||||||
|
{schedule.shellType}
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
{schedule.command && (
|
||||||
<Terminal className="size-3.5 text-muted-foreground/70" />
|
<div className="flex items-center gap-2">
|
||||||
<code className="font-mono text-[10px] text-muted-foreground/70">
|
<Terminal className="size-3.5 text-muted-foreground/70" />
|
||||||
{schedule.command}
|
<code className="font-mono text-[10px] text-muted-foreground/70">
|
||||||
</code>
|
{schedule.command}
|
||||||
</div>
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ShowSchedulesLogs
|
<ShowSchedulesLogs
|
||||||
deployments={deployments || []}
|
deployments={deployments || []}
|
||||||
serverId={application.serverId || undefined}
|
serverId={serverId}
|
||||||
>
|
>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<ClipboardList className="size-4 transition-colors " />
|
<ClipboardList className="size-4 transition-colors " />
|
||||||
@@ -146,15 +167,10 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Schedule run successfully");
|
toast.success("Schedule run successfully");
|
||||||
utils.schedule.list.invalidate({
|
refetchSchedules();
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.log(error);
|
toast.error("Error running schedule:");
|
||||||
toast.error(
|
|
||||||
`Error running schedule: ${error}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -167,7 +183,8 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
<HandleSchedules
|
<HandleSchedules
|
||||||
scheduleId={schedule.scheduleId}
|
scheduleId={schedule.scheduleId}
|
||||||
applicationId={applicationId}
|
id={id}
|
||||||
|
scheduleType={scheduleType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -180,7 +197,8 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
utils.schedule.list.invalidate({
|
utils.schedule.list.invalidate({
|
||||||
applicationId,
|
id,
|
||||||
|
scheduleType,
|
||||||
});
|
});
|
||||||
toast.success("Schedule deleted successfully");
|
toast.success("Schedule deleted successfully");
|
||||||
})
|
})
|
||||||
@@ -204,7 +222,7 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2 items-center justify-center py-12 border rounded-lg">
|
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||||
<Clock className="size-8 mb-4 text-muted-foreground" />
|
<Clock className="size-8 mb-4 text-muted-foreground" />
|
||||||
<p className="text-lg font-medium text-muted-foreground">
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
No scheduled tasks
|
No scheduled tasks
|
||||||
@@ -212,7 +230,7 @@ export const ShowSchedules = ({ applicationId }: Props) => {
|
|||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Create your first scheduled task to automate your workflows
|
Create your first scheduled task to automate your workflows
|
||||||
</p>
|
</p>
|
||||||
<HandleSchedules applicationId={applicationId} />
|
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowSchedulesModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Schedules
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
||||||
|
<ShowSchedules id={serverId} scheduleType="server" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -43,6 +43,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
|||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
|
import { ShowSchedulesModal } from "./show-schedules-modal";
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
@@ -332,6 +333,10 @@ export const ShowServers = () => {
|
|||||||
<ShowNodesModal
|
<ShowNodesModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ShowSchedulesModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -158,6 +159,14 @@ const MENU: Menu = {
|
|||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud }) => !isCloud,
|
isEnabled: ({ isCloud }) => !isCloud,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
isSingle: true,
|
||||||
|
title: "Schedules",
|
||||||
|
url: "/dashboard/schedules",
|
||||||
|
icon: Clock,
|
||||||
|
// Only enabled in non-cloud environments
|
||||||
|
isEnabled: ({ isCloud }) => !isCloud,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
|
|||||||
8
apps/dokploy/drizzle/0093_abnormal_machine_man.sql
Normal file
8
apps/dokploy/drizzle/0093_abnormal_machine_man.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TYPE "public"."scheduleType" AS ENUM('application', 'compose', 'server');--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ALTER COLUMN "applicationId" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD COLUMN "scheduleType" "scheduleType" DEFAULT 'application' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD COLUMN "composeId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
1
apps/dokploy/drizzle/0094_easy_butterfly.sql
Normal file
1
apps/dokploy/drizzle/0094_easy_butterfly.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "schedule" ADD COLUMN "script" text;
|
||||||
3
apps/dokploy/drizzle/0095_friendly_cobalt_man.sql
Normal file
3
apps/dokploy/drizzle/0095_friendly_cobalt_man.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TYPE "public"."scheduleType" ADD VALUE 'dokploy-server';--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD COLUMN "userId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
5579
apps/dokploy/drizzle/meta/0093_snapshot.json
Normal file
5579
apps/dokploy/drizzle/meta/0093_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5585
apps/dokploy/drizzle/meta/0094_snapshot.json
Normal file
5585
apps/dokploy/drizzle/meta/0094_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5605
apps/dokploy/drizzle/meta/0095_snapshot.json
Normal file
5605
apps/dokploy/drizzle/meta/0095_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -652,6 +652,27 @@
|
|||||||
"when": 1746221961240,
|
"when": 1746221961240,
|
||||||
"tag": "0092_safe_scarlet_witch",
|
"tag": "0092_safe_scarlet_witch",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 93,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1746228754403,
|
||||||
|
"tag": "0093_abnormal_machine_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 94,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1746228771046,
|
||||||
|
"tag": "0094_easy_butterfly",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 95,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1746232483345,
|
||||||
|
"tag": "0095_friendly_cobalt_man",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -312,7 +312,10 @@ const Service = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="schedules">
|
<TabsContent value="schedules">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowSchedules applicationId={applicationId} />
|
<ShowSchedules
|
||||||
|
id={applicationId}
|
||||||
|
scheduleType="application"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments" className="w-full">
|
<TabsContent value="deployments" className="w-full">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
||||||
@@ -217,10 +218,10 @@ const Service = (
|
|||||||
className={cn(
|
className={cn(
|
||||||
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
isCloud && data?.serverId
|
isCloud && data?.serverId
|
||||||
? "lg:grid-cols-7"
|
? "lg:grid-cols-8"
|
||||||
: data?.serverId
|
: data?.serverId
|
||||||
? "lg:grid-cols-6"
|
? "lg:grid-cols-7"
|
||||||
: "lg:grid-cols-7",
|
: "lg:grid-cols-8",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@@ -228,6 +229,7 @@ const Service = (
|
|||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
@@ -246,6 +248,12 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="schedules">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowSchedules id={composeId} scheduleType="compose" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="pt-2.5">
|
<div className="pt-2.5">
|
||||||
<div className="flex flex-col border rounded-lg ">
|
<div className="flex flex-col border rounded-lg ">
|
||||||
|
|||||||
54
apps/dokploy/pages/dashboard/schedules.tsx
Normal file
54
apps/dokploy/pages/dashboard/schedules.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
function SchedulesPage() {
|
||||||
|
const { data: user } = api.user.get.useQuery();
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
||||||
|
<div className="rounded-xl bg-background shadow-md h-full">
|
||||||
|
<ShowSchedules
|
||||||
|
scheduleType="dokploy-server"
|
||||||
|
id={user?.user.id || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default SchedulesPage;
|
||||||
|
|
||||||
|
SchedulesPage.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout>{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { user } = await validateRequest(ctx.req);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,67 +6,65 @@ import {
|
|||||||
updateScheduleSchema,
|
updateScheduleSchema,
|
||||||
} from "@dokploy/server/db/schema/schedule";
|
} from "@dokploy/server/db/schema/schedule";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
import { db } from "@dokploy/server/db";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { runCommand } from "@dokploy/server/index";
|
import { runCommand } from "@dokploy/server/index";
|
||||||
import { deployments } from "@dokploy/server/db/schema/deployment";
|
import { deployments } from "@dokploy/server/db/schema/deployment";
|
||||||
|
import {
|
||||||
|
deleteSchedule,
|
||||||
|
findScheduleById,
|
||||||
|
createSchedule,
|
||||||
|
updateSchedule,
|
||||||
|
} from "@dokploy/server/services/schedule";
|
||||||
|
|
||||||
export const scheduleRouter = createTRPCRouter({
|
export const scheduleRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(createScheduleSchema)
|
.input(createScheduleSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { scheduleId, ...rest } = input;
|
const schedule = await createSchedule(input);
|
||||||
const [schedule] = await ctx.db
|
|
||||||
.insert(schedules)
|
|
||||||
.values(rest)
|
|
||||||
.returning();
|
|
||||||
return schedule;
|
return schedule;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(updateScheduleSchema)
|
.input(updateScheduleSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { scheduleId, ...rest } = input;
|
const schedule = await updateSchedule(input);
|
||||||
const [schedule] = await ctx.db
|
|
||||||
.update(schedules)
|
|
||||||
.set(rest)
|
|
||||||
.where(eq(schedules.scheduleId, scheduleId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!schedule) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Schedule not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return schedule;
|
return schedule;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(z.object({ scheduleId: z.string() }))
|
.input(z.object({ scheduleId: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const [schedule] = await ctx.db
|
await deleteSchedule(input.scheduleId);
|
||||||
.delete(schedules)
|
|
||||||
.where(eq(schedules.scheduleId, input.scheduleId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!schedule) {
|
return true;
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Schedule not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return schedule;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(z.object({ applicationId: z.string() }))
|
.input(
|
||||||
.query(async ({ ctx, input }) => {
|
z.object({
|
||||||
return ctx.db.query.schedules.findMany({
|
id: z.string(),
|
||||||
where: eq(schedules.applicationId, input.applicationId),
|
scheduleType: z.enum([
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server",
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const where = {
|
||||||
|
application: eq(schedules.applicationId, input.id),
|
||||||
|
compose: eq(schedules.composeId, input.id),
|
||||||
|
server: eq(schedules.serverId, input.id),
|
||||||
|
"dokploy-server": eq(schedules.userId, input.id),
|
||||||
|
};
|
||||||
|
return db.query.schedules.findMany({
|
||||||
|
where: where[input.scheduleType],
|
||||||
with: {
|
with: {
|
||||||
application: true,
|
application: true,
|
||||||
|
server: true,
|
||||||
|
compose: true,
|
||||||
deployments: {
|
deployments: {
|
||||||
orderBy: [desc(deployments.createdAt)],
|
orderBy: [desc(deployments.createdAt)],
|
||||||
},
|
},
|
||||||
@@ -76,20 +74,8 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(z.object({ scheduleId: z.string() }))
|
.input(z.object({ scheduleId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ input }) => {
|
||||||
const [schedule] = await ctx.db
|
return await findScheduleById(input.scheduleId);
|
||||||
.select()
|
|
||||||
.from(schedules)
|
|
||||||
.where(eq(schedules.scheduleId, input.scheduleId));
|
|
||||||
|
|
||||||
if (!schedule) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Schedule not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return schedule;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
runManually: protectedProcedure
|
runManually: protectedProcedure
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { server } from "./server";
|
|||||||
import { applicationStatus, triggerType } from "./shared";
|
import { applicationStatus, triggerType } from "./shared";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { schedules } from "./schedule";
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
"github",
|
"github",
|
||||||
@@ -135,6 +135,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
|||||||
fields: [compose.serverId],
|
fields: [compose.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
|
schedules: many(schedules),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(compose, {
|
const createSchema = createInsertSchema(compose, {
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ import { z } from "zod";
|
|||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { compose } from "./compose";
|
||||||
|
import { server } from "./server";
|
||||||
|
import { users_temp } from "./user";
|
||||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||||
|
|
||||||
|
export const scheduleType = pgEnum("scheduleType", [
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server",
|
||||||
|
]);
|
||||||
|
|
||||||
export const schedules = pgTable("schedule", {
|
export const schedules = pgTable("schedule", {
|
||||||
scheduleId: text("scheduleId")
|
scheduleId: text("scheduleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -19,13 +28,26 @@ export const schedules = pgTable("schedule", {
|
|||||||
appName: text("appName")
|
appName: text("appName")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => generateAppName("schedule")),
|
.$defaultFn(() => generateAppName("schedule")),
|
||||||
|
serviceName: text("serviceName"),
|
||||||
shellType: shellTypes("shellType").notNull().default("bash"),
|
shellType: shellTypes("shellType").notNull().default("bash"),
|
||||||
|
scheduleType: scheduleType("scheduleType").notNull().default("application"),
|
||||||
command: text("command").notNull(),
|
command: text("command").notNull(),
|
||||||
applicationId: text("applicationId")
|
script: text("script"),
|
||||||
.notNull()
|
applicationId: text("applicationId").references(
|
||||||
.references(() => applications.applicationId, {
|
() => applications.applicationId,
|
||||||
|
{
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
|
composeId: text("composeId").references(() => compose.composeId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
userId: text("userId").references(() => users_temp.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -39,15 +61,22 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
|||||||
fields: [schedules.applicationId],
|
fields: [schedules.applicationId],
|
||||||
references: [applications.applicationId],
|
references: [applications.applicationId],
|
||||||
}),
|
}),
|
||||||
|
compose: one(compose, {
|
||||||
|
fields: [schedules.composeId],
|
||||||
|
references: [compose.composeId],
|
||||||
|
}),
|
||||||
|
server: one(server, {
|
||||||
|
fields: [schedules.serverId],
|
||||||
|
references: [server.serverId],
|
||||||
|
}),
|
||||||
|
user: one(users_temp, {
|
||||||
|
fields: [schedules.userId],
|
||||||
|
references: [users_temp.id],
|
||||||
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const createScheduleSchema = createInsertSchema(schedules, {
|
export const createScheduleSchema = createInsertSchema(schedules);
|
||||||
name: z.string().min(1),
|
|
||||||
cronExpression: z.string().min(1),
|
|
||||||
command: z.string().min(1),
|
|
||||||
applicationId: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateScheduleSchema = createUpdateSchema(schedules).extend({
|
export const updateScheduleSchema = createUpdateSchema(schedules).extend({
|
||||||
scheduleId: z.string().min(1),
|
scheduleId: z.string().min(1),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { postgres } from "./postgres";
|
|||||||
import { redis } from "./redis";
|
import { redis } from "./redis";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { schedules } from "./schedule";
|
||||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||||
|
|
||||||
export const server = pgTable("server", {
|
export const server = pgTable("server", {
|
||||||
@@ -114,6 +114,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
|
|||||||
fields: [server.organizationId],
|
fields: [server.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
}),
|
}),
|
||||||
|
schedules: many(schedules),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(server, {
|
const createSchema = createInsertSchema(server, {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { account, apikey, organization } from "./account";
|
|||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { certificateType } from "./shared";
|
import { certificateType } from "./shared";
|
||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
|
import { schedules } from "./schedule";
|
||||||
/**
|
/**
|
||||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||||
* database instance for multiple projects.
|
* database instance for multiple projects.
|
||||||
@@ -127,6 +128,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
|||||||
projects: many(projects),
|
projects: many(projects),
|
||||||
apiKeys: many(apikey),
|
apiKeys: many(apikey),
|
||||||
backups: many(backups),
|
backups: many(backups),
|
||||||
|
schedules: many(schedules),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(users_temp, {
|
const createSchema = createInsertSchema(users_temp, {
|
||||||
|
|||||||
@@ -281,17 +281,19 @@ export const createDeploymentSchedule = async (
|
|||||||
const schedule = await findScheduleById(deployment.scheduleId);
|
const schedule = await findScheduleById(deployment.scheduleId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeDeploymentsSchedule(
|
const serverId =
|
||||||
deployment.scheduleId,
|
schedule.application?.serverId ||
|
||||||
schedule.application.serverId,
|
schedule.compose?.serverId ||
|
||||||
);
|
schedule.server?.serverId;
|
||||||
const { SCHEDULES_PATH } = paths(!!schedule.application.serverId);
|
await removeDeploymentsSchedule(deployment.scheduleId, serverId);
|
||||||
|
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
||||||
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
|
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
|
||||||
|
|
||||||
if (schedule.application.serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(schedule.application.serverId);
|
console.log("serverId", serverId);
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
const command = `
|
const command = `
|
||||||
mkdir -p ${SCHEDULES_PATH}/${schedule.appName};
|
mkdir -p ${SCHEDULES_PATH}/${schedule.appName};
|
||||||
@@ -324,6 +326,7 @@ export const createDeploymentSchedule = async (
|
|||||||
}
|
}
|
||||||
return deploymentCreate[0];
|
return deploymentCreate[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
await db
|
await db
|
||||||
.insert(deployments)
|
.insert(deployments)
|
||||||
.values({
|
.values({
|
||||||
@@ -476,7 +479,7 @@ export const removeLastTenPreviewDeploymenById = async (
|
|||||||
|
|
||||||
export const removeDeploymentsSchedule = async (
|
export const removeDeploymentsSchedule = async (
|
||||||
scheduleId: string,
|
scheduleId: string,
|
||||||
serverId: string | null,
|
serverId?: string | null,
|
||||||
) => {
|
) => {
|
||||||
const deploymentList = await db.query.deployments.findMany({
|
const deploymentList = await db.query.deployments.findMany({
|
||||||
where: eq(deployments.scheduleId, scheduleId),
|
where: eq(deployments.scheduleId, scheduleId),
|
||||||
|
|||||||
@@ -1,13 +1,41 @@
|
|||||||
import { schedules } from "../db/schema/schedule";
|
import { type Schedule, schedules } from "../db/schema/schedule";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import type { z } from "zod";
|
||||||
|
import type {
|
||||||
|
createScheduleSchema,
|
||||||
|
updateScheduleSchema,
|
||||||
|
} from "../db/schema/schedule";
|
||||||
|
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||||
|
import { paths } from "../constants";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
|
||||||
|
|
||||||
|
export const createSchedule = async (
|
||||||
|
input: z.infer<typeof createScheduleSchema>,
|
||||||
|
) => {
|
||||||
|
const { scheduleId, ...rest } = input;
|
||||||
|
const [newSchedule] = await db.insert(schedules).values(rest).returning();
|
||||||
|
|
||||||
|
if (
|
||||||
|
newSchedule &&
|
||||||
|
(newSchedule.scheduleType === "dokploy-server" ||
|
||||||
|
newSchedule.scheduleType === "server")
|
||||||
|
) {
|
||||||
|
await handleScript(newSchedule);
|
||||||
|
}
|
||||||
|
return newSchedule;
|
||||||
|
};
|
||||||
|
|
||||||
export const findScheduleById = async (scheduleId: string) => {
|
export const findScheduleById = async (scheduleId: string) => {
|
||||||
const schedule = await db.query.schedules.findFirst({
|
const schedule = await db.query.schedules.findFirst({
|
||||||
where: eq(schedules.scheduleId, scheduleId),
|
where: eq(schedules.scheduleId, scheduleId),
|
||||||
with: {
|
with: {
|
||||||
application: true,
|
application: true,
|
||||||
|
compose: true,
|
||||||
|
server: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,3 +47,66 @@ export const findScheduleById = async (scheduleId: string) => {
|
|||||||
}
|
}
|
||||||
return schedule;
|
return schedule;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteSchedule = async (scheduleId: string) => {
|
||||||
|
const schedule = await findScheduleById(scheduleId);
|
||||||
|
|
||||||
|
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
|
||||||
|
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||||
|
const command = `rm -rf ${fullPath}`;
|
||||||
|
if (schedule.serverId) {
|
||||||
|
await execAsyncRemote(schedule.serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleResult = await db
|
||||||
|
.delete(schedules)
|
||||||
|
.where(eq(schedules.scheduleId, scheduleId));
|
||||||
|
if (!scheduleResult) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Schedule not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSchedule = async (
|
||||||
|
input: z.infer<typeof updateScheduleSchema>,
|
||||||
|
) => {
|
||||||
|
const { scheduleId, ...rest } = input;
|
||||||
|
const [updatedSchedule] = await db
|
||||||
|
.update(schedules)
|
||||||
|
.set(rest)
|
||||||
|
.where(eq(schedules.scheduleId, scheduleId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedSchedule?.scheduleType === "dokploy-server" ||
|
||||||
|
updatedSchedule?.scheduleType === "server"
|
||||||
|
) {
|
||||||
|
await handleScript(updatedSchedule);
|
||||||
|
}
|
||||||
|
return updatedSchedule;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScript = async (schedule: Schedule) => {
|
||||||
|
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
|
||||||
|
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
mkdir -p ${fullPath}
|
||||||
|
rm -f ${fullPath}/script.sh
|
||||||
|
touch ${fullPath}/script.sh
|
||||||
|
chmod +x ${fullPath}/script.sh
|
||||||
|
echo "${schedule?.script}" > ${fullPath}/script.sh
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (schedule?.scheduleType === "dokploy-server") {
|
||||||
|
await execAsync(script);
|
||||||
|
} else if (schedule?.scheduleType === "server") {
|
||||||
|
await execAsyncRemote(schedule?.serverId || "", script);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { RedisNested } from "../databases/redis";
|
|||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
import { spawnAsync } from "../process/spawnAsync";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
|
import type { Compose } from "@dokploy/server/services/compose";
|
||||||
|
|
||||||
interface RegistryAuth {
|
interface RegistryAuth {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -541,3 +542,67 @@ export const getRemoteServiceContainer = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServiceContainerIV2 = async (
|
||||||
|
appName: string,
|
||||||
|
serverId?: string | null,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const filter = {
|
||||||
|
status: ["running"],
|
||||||
|
label: [`com.docker.swarm.service.name=${appName}`],
|
||||||
|
};
|
||||||
|
const remoteDocker = await getRemoteDocker(serverId);
|
||||||
|
const containers = await remoteDocker.listContainers({
|
||||||
|
filters: JSON.stringify(filter),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (containers.length === 0 || !containers[0]) {
|
||||||
|
throw new Error(`No container found with name: ${appName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = containers[0];
|
||||||
|
return container;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getComposeContainer = async (
|
||||||
|
compose: Compose,
|
||||||
|
serviceName: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { appName, composeType, serverId } = compose;
|
||||||
|
// 1. Determine the correct labels based on composeType
|
||||||
|
const labels: string[] = [];
|
||||||
|
if (composeType === "stack") {
|
||||||
|
// Labels for Docker Swarm stack services
|
||||||
|
labels.push(`com.docker.stack.namespace=${appName}`);
|
||||||
|
labels.push(`com.docker.swarm.service.name=${appName}_${serviceName}`);
|
||||||
|
} else {
|
||||||
|
// Labels for Docker Compose projects (default)
|
||||||
|
labels.push(`com.docker.compose.project=${appName}`);
|
||||||
|
labels.push(`com.docker.compose.service=${serviceName}`);
|
||||||
|
}
|
||||||
|
const filter = {
|
||||||
|
status: ["running"],
|
||||||
|
label: labels,
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteDocker = await getRemoteDocker(serverId);
|
||||||
|
const containers = await remoteDocker.listContainers({
|
||||||
|
filters: JSON.stringify(filter),
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (containers.length === 0 || !containers[0]) {
|
||||||
|
throw new Error(`No container found with name: ${appName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = containers[0];
|
||||||
|
return container;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Schedule } from "@dokploy/server/db/schema/schedule";
|
import type { Schedule } from "@dokploy/server/db/schema/schedule";
|
||||||
import { findScheduleById } from "@dokploy/server/services/schedule";
|
import { findScheduleById } from "@dokploy/server/services/schedule";
|
||||||
import { scheduleJob as scheduleJobNode } from "node-schedule";
|
import { scheduleJob as scheduleJobNode } from "node-schedule";
|
||||||
import {
|
import { getComposeContainer, getServiceContainerIV2 } from "../docker/utils";
|
||||||
getRemoteServiceContainer,
|
|
||||||
getServiceContainer,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
import { spawnAsync } from "../process/spawnAsync";
|
||||||
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
|
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
|
||||||
import { createWriteStream } from "node:fs";
|
import { createWriteStream } from "node:fs";
|
||||||
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
|
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
|
||||||
|
import { paths } from "@dokploy/server/constants";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
export const scheduleJob = (schedule: Schedule) => {
|
export const scheduleJob = (schedule: Schedule) => {
|
||||||
const { cronExpression, scheduleId } = schedule;
|
const { cronExpression, scheduleId } = schedule;
|
||||||
|
|
||||||
@@ -19,17 +19,16 @@ export const scheduleJob = (schedule: Schedule) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const runCommand = async (scheduleId: string) => {
|
export const runCommand = async (scheduleId: string) => {
|
||||||
const { application, command, shellType } =
|
const {
|
||||||
await findScheduleById(scheduleId);
|
application,
|
||||||
|
command,
|
||||||
const isServer = !!application.serverId;
|
shellType,
|
||||||
|
scheduleType,
|
||||||
const { Id: containerId } = isServer
|
compose,
|
||||||
? await getRemoteServiceContainer(
|
serviceName,
|
||||||
application.serverId || "",
|
appName,
|
||||||
application.appName,
|
serverId,
|
||||||
)
|
} = await findScheduleById(scheduleId);
|
||||||
: await getServiceContainer(application.appName);
|
|
||||||
|
|
||||||
const deployment = await createDeploymentSchedule({
|
const deployment = await createDeploymentSchedule({
|
||||||
scheduleId,
|
scheduleId,
|
||||||
@@ -37,51 +36,109 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
description: "Schedule",
|
description: "Schedule",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isServer) {
|
if (scheduleType === "application" || scheduleType === "compose") {
|
||||||
try {
|
let containerId = "";
|
||||||
await execAsyncRemote(
|
let serverId = "";
|
||||||
|
if (scheduleType === "application" && application) {
|
||||||
|
const container = await getServiceContainerIV2(
|
||||||
|
application.appName,
|
||||||
application.serverId,
|
application.serverId,
|
||||||
`
|
|
||||||
set -e
|
|
||||||
echo "Running command: docker exec ${containerId} ${shellType} -c \"${command}\"" >> ${deployment.logPath};
|
|
||||||
docker exec ${containerId} ${shellType} -c "${command}" || {
|
|
||||||
echo "❌ Command failed" >> ${deployment.logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
containerId = container.Id;
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
serverId = application.serverId || "";
|
||||||
throw error;
|
}
|
||||||
|
if (scheduleType === "compose" && compose) {
|
||||||
|
const container = await getComposeContainer(compose, serviceName || "");
|
||||||
|
containerId = container.Id;
|
||||||
|
serverId = compose.serverId || "";
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
try {
|
||||||
|
await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`
|
||||||
|
set -e
|
||||||
|
echo "Running command: docker exec ${containerId} ${shellType} -c \"${command}\"" >> ${deployment.logPath};
|
||||||
|
docker exec ${containerId} ${shellType} -c "${command}" >> ${deployment.logPath} 2>> ${deployment.logPath} || {
|
||||||
|
echo "❌ Command failed" >> ${deployment.logPath};
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
echo "✅ Command executed successfully" >> ${deployment.logPath};
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeStream.write(
|
||||||
|
`docker exec ${containerId} ${shellType} -c "${command}"\n`,
|
||||||
|
);
|
||||||
|
await spawnAsync(
|
||||||
|
"docker",
|
||||||
|
["exec", containerId, shellType, "-c", command],
|
||||||
|
(data) => {
|
||||||
|
if (writeStream.writable) {
|
||||||
|
writeStream.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
writeStream.write("✅ Command executed successfully\n");
|
||||||
|
} catch (error) {
|
||||||
|
writeStream.write("❌ Command failed\n");
|
||||||
|
writeStream.write(
|
||||||
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
|
);
|
||||||
|
writeStream.end();
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (scheduleType === "dokploy-server") {
|
||||||
try {
|
try {
|
||||||
writeStream.write(
|
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||||
`docker exec ${containerId} ${shellType} -c "${command}"\n`,
|
const { SCHEDULES_PATH } = paths();
|
||||||
);
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
|
|
||||||
await spawnAsync(
|
await spawnAsync(
|
||||||
"docker",
|
"bash",
|
||||||
["exec", containerId, shellType, "-c", command],
|
["-c", "./script.sh"],
|
||||||
(data) => {
|
(data) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
writeStream.write(data);
|
writeStream.write(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cwd: fullPath,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
writeStream.write("✅ Command executed successfully\n");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeStream.write("❌ Command failed\n");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
writeStream.write(
|
throw error;
|
||||||
error instanceof Error ? error.message : "Unknown error",
|
}
|
||||||
);
|
} else if (scheduleType === "server") {
|
||||||
writeStream.end();
|
try {
|
||||||
|
const { SCHEDULES_PATH } = paths(true);
|
||||||
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
|
const command = `
|
||||||
|
set -e
|
||||||
|
echo "Running script" >> ${deployment.logPath};
|
||||||
|
bash -c ${fullPath}/script.sh >> ${deployment.logPath} 2>> ${deployment.logPath} || {
|
||||||
|
echo "❌ Command failed" >> ${deployment.logPath};
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
echo "✅ Command executed successfully" >> ${deployment.logPath};
|
||||||
|
`;
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} catch (error) {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user