mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-25 17:15:22 +02:00
Merge branch 'canary' into filter-projects-shortcut
This commit is contained in:
@@ -3,8 +3,8 @@ import { z } from "zod";
|
||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
previewDeploymentId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
|
||||
@@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ export const deploy = async (job: DeployJob) => {
|
||||
if (job.type === "deploy") {
|
||||
await deployRemotePreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
titleLog: job.titleLog || "Preview Deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
previewDeploymentId: job.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.application.create.useMutation();
|
||||
@@ -94,8 +98,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
description: data.description,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
environmentId,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Created");
|
||||
@@ -157,7 +161,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{hasServers && (
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
@@ -186,13 +190,27 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -206,7 +224,9 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -82,6 +82,10 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const form = useForm<AddCompose>({
|
||||
defaultValues: {
|
||||
@@ -104,7 +108,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
environmentId,
|
||||
composeType: data.composeType,
|
||||
appName: data.appName,
|
||||
serverId: data.serverId,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
@@ -169,7 +173,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{hasServers && (
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
@@ -198,13 +202,27 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -218,7 +236,9 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -179,6 +179,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
@@ -190,6 +191,10 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const form = useForm<AddDatabase>({
|
||||
defaultValues: {
|
||||
@@ -223,9 +228,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
dockerImage: defaultDockerImage,
|
||||
projectId: environment?.projectId || "",
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
environmentId,
|
||||
serverId: data.serverId,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
@@ -237,7 +241,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
});
|
||||
} else if (data.type === "mongo") {
|
||||
promise = mongoMutation.mutateAsync({
|
||||
@@ -245,14 +249,14 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
databasePassword: data.databasePassword,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
replicaSets: data.replicaSets,
|
||||
});
|
||||
} else if (data.type === "redis") {
|
||||
promise = redisMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
serverId: data.serverId,
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
});
|
||||
} else if (data.type === "mariadb") {
|
||||
promise = mariadbMutation.mutateAsync({
|
||||
@@ -262,7 +266,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
databaseName: data.databaseName || "mariadb",
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
});
|
||||
} else if (data.type === "mysql") {
|
||||
promise = mysqlMutation.mutateAsync({
|
||||
@@ -271,8 +275,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
databaseName: data.databaseName || "mysql",
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
databaseRootPassword: data.databaseRootPassword || "",
|
||||
serverId: data.serverId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -403,7 +407,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{hasServers && (
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
@@ -412,13 +416,29 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
<FormLabel>Select a Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!isCloud ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -428,7 +448,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length})
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
|
||||
@@ -141,6 +141,10 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
}) || [];
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -430,7 +434,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
project.
|
||||
</AlertDialogDescription>
|
||||
|
||||
{hasServers && (
|
||||
{shouldShowServerDropdown && (
|
||||
<div>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
@@ -459,12 +463,29 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
onValueChange={(e) => {
|
||||
setServerId(e);
|
||||
}}
|
||||
defaultValue={
|
||||
!isCloud ? "dokploy" : undefined
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!isCloud ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
@@ -479,7 +500,8 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length})
|
||||
Servers (
|
||||
{servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
@@ -493,8 +515,11 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
const promise = mutateAsync({
|
||||
serverId:
|
||||
serverId === "dokploy"
|
||||
? undefined
|
||||
: serverId,
|
||||
environmentId,
|
||||
serverId: serverId || undefined,
|
||||
id: template.id,
|
||||
baseUrl: customBaseUrl,
|
||||
});
|
||||
|
||||
@@ -25,7 +25,12 @@ const examples = [
|
||||
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||
// Get servers from the API
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const handleExampleClick = (example: string) => {
|
||||
setTemplateInfo({ ...templateInfo, userInput: example });
|
||||
@@ -48,34 +53,58 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasServers && (
|
||||
{shouldShowServerDropdown && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server-deploy">
|
||||
Select the server where you want to deploy (optional)
|
||||
</Label>
|
||||
<Select
|
||||
value={templateInfo.server?.serverId}
|
||||
value={
|
||||
templateInfo.server?.serverId ||
|
||||
(!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
const server = servers?.find((s) => s.serverId === value);
|
||||
if (server) {
|
||||
if (value === "dokploy") {
|
||||
setTemplateInfo({
|
||||
...templateInfo,
|
||||
server: server,
|
||||
server: undefined,
|
||||
});
|
||||
} else {
|
||||
const server = servers?.find((s) => s.serverId === value);
|
||||
if (server) {
|
||||
setTemplateInfo({
|
||||
...templateInfo,
|
||||
server: server,
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a server" />
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem key={server.serverId} value={server.serverId}>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -65,6 +65,11 @@ export const AddCertificate = () => {
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.certificates.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
// Cloud: show only if there are remote servers (no Dokploy option)
|
||||
// Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers)
|
||||
const shouldShowServerDropdown = hasServers;
|
||||
|
||||
const form = useForm<AddCertificate>({
|
||||
defaultValues: {
|
||||
@@ -85,7 +90,7 @@ export const AddCertificate = () => {
|
||||
certificateData: data.certificateData,
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
serverId: data.serverId,
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
organizationId: "",
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -174,52 +179,70 @@ export const AddCertificate = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server {!isCloud && "(Optional)"}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server {!isCloud && "(Optional)"}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{server.name}</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
{server.ipAddress}
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{server.name}</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
{server.ipAddress}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-end">
|
||||
|
||||
@@ -101,6 +101,15 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
decoration: z.boolean().default(true),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("ntfy"),
|
||||
serverUrl: z.string().min(1, { message: "Server URL is required" }),
|
||||
topic: z.string().min(1, { message: "Topic is required" }),
|
||||
accessToken: z.string().min(1, { message: "Access Token is required" }),
|
||||
priority: z.number().min(1).max(5).default(3),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
]);
|
||||
|
||||
export const notificationsMap = {
|
||||
@@ -124,6 +133,10 @@ export const notificationsMap = {
|
||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
||||
label: "Gotify",
|
||||
},
|
||||
ntfy: {
|
||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
||||
label: "ntfy",
|
||||
},
|
||||
};
|
||||
|
||||
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||
@@ -155,6 +168,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
api.notification.testEmailConnection.useMutation();
|
||||
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
|
||||
api.notification.testGotifyConnection.useMutation();
|
||||
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
||||
api.notification.testNtfyConnection.useMutation();
|
||||
const slackMutation = notificationId
|
||||
? api.notification.updateSlack.useMutation()
|
||||
: api.notification.createSlack.useMutation();
|
||||
@@ -170,6 +185,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
const gotifyMutation = notificationId
|
||||
? api.notification.updateGotify.useMutation()
|
||||
: api.notification.createGotify.useMutation();
|
||||
const ntfyMutation = notificationId
|
||||
? api.notification.updateNtfy.useMutation()
|
||||
: api.notification.createNtfy.useMutation();
|
||||
|
||||
const form = useForm<NotificationSchema>({
|
||||
defaultValues: {
|
||||
@@ -266,6 +284,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
});
|
||||
} else if (notification.notificationType === "ntfy") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
type: notification.notificationType,
|
||||
accessToken: notification.ntfy?.accessToken,
|
||||
topic: notification.ntfy?.topic,
|
||||
priority: notification.ntfy?.priority,
|
||||
serverUrl: notification.ntfy?.serverUrl,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset();
|
||||
@@ -278,6 +310,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
discord: discordMutation,
|
||||
email: emailMutation,
|
||||
gotify: gotifyMutation,
|
||||
ntfy: ntfyMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: NotificationSchema) => {
|
||||
@@ -366,6 +399,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
notificationId: notificationId || "",
|
||||
gotifyId: notification?.gotifyId || "",
|
||||
});
|
||||
} else if (data.type === "ntfy") {
|
||||
promise = ntfyMutation.mutateAsync({
|
||||
appBuildError: appBuildError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
serverUrl: data.serverUrl,
|
||||
accessToken: data.accessToken,
|
||||
topic: data.topic,
|
||||
priority: data.priority,
|
||||
name: data.name,
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
ntfyId: notification?.ntfyId || "",
|
||||
});
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
@@ -875,6 +923,83 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "ntfy" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://ntfy.sh" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="topic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Topic</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="deployments" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Access Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="AzxcvbnmKjhgfdsa..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
defaultValue={3}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Priority</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="3"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value) {
|
||||
const port = Number.parseInt(value);
|
||||
if (port > 0 && port <= 5) {
|
||||
field.onChange(port);
|
||||
}
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Message priority (1-5, default: 3)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -1024,7 +1149,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
isLoadingTelegram ||
|
||||
isLoadingDiscord ||
|
||||
isLoadingEmail ||
|
||||
isLoadingGotify
|
||||
isLoadingGotify ||
|
||||
isLoadingNtfy
|
||||
}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
@@ -1061,6 +1187,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
priority: form.getValues("priority"),
|
||||
decoration: form.getValues("decoration"),
|
||||
});
|
||||
} else if (type === "ntfy") {
|
||||
await testNtfyConnection({
|
||||
serverUrl: form.getValues("serverUrl"),
|
||||
topic: form.getValues("topic"),
|
||||
accessToken: form.getValues("accessToken"),
|
||||
priority: form.getValues("priority"),
|
||||
});
|
||||
}
|
||||
toast.success("Connection Success");
|
||||
} catch {
|
||||
|
||||
@@ -88,6 +88,11 @@ export const ShowNotifications = () => {
|
||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{notification.notificationType === "ntfy" && (
|
||||
<div className="flex items-center justify-center rounded-lg ">
|
||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notification.name}
|
||||
</span>
|
||||
|
||||
11
apps/dokploy/drizzle/0110_red_psynapse.sql
Normal file
11
apps/dokploy/drizzle/0110_red_psynapse.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
ALTER TYPE "public"."notificationType" ADD VALUE 'ntfy';--> statement-breakpoint
|
||||
CREATE TABLE "ntfy" (
|
||||
"ntfyId" text PRIMARY KEY NOT NULL,
|
||||
"serverUrl" text NOT NULL,
|
||||
"topic" text NOT NULL,
|
||||
"accessToken" text NOT NULL,
|
||||
"priority" integer DEFAULT 3 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "ntfyId" text;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD CONSTRAINT "notification_ntfyId_ntfy_ntfyId_fk" FOREIGN KEY ("ntfyId") REFERENCES "public"."ntfy"("ntfyId") ON DELETE cascade ON UPDATE no action;
|
||||
6559
apps/dokploy/drizzle/meta/0110_snapshot.json
Normal file
6559
apps/dokploy/drizzle/meta/0110_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -771,6 +771,13 @@
|
||||
"when": 1757052053574,
|
||||
"tag": "0109_remarkable_sauron",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 110,
|
||||
"version": "7",
|
||||
"when": 1757189541734,
|
||||
"tag": "0110_red_psynapse",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -40,8 +40,10 @@ import {
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateApplication,
|
||||
apiDeployApplication,
|
||||
apiFindMonitoringStats,
|
||||
apiFindOneApplication,
|
||||
apiRedeployApplication,
|
||||
apiReloadApplication,
|
||||
apiSaveBitbucketProvider,
|
||||
apiSaveBuildType,
|
||||
@@ -306,7 +308,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.input(apiRedeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
@@ -320,8 +322,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Rebuild deployment",
|
||||
descriptionLog: "",
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
descriptionLog: input.description || "",
|
||||
type: "redeploy",
|
||||
applicationType: "application",
|
||||
server: !!application.serverId,
|
||||
@@ -670,7 +672,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.input(apiDeployApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
@@ -684,8 +686,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
descriptionLog: "",
|
||||
titleLog: input.title || "Manual deployment",
|
||||
descriptionLog: input.description || "",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!application.serverId,
|
||||
|
||||
@@ -48,9 +48,11 @@ import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateCompose,
|
||||
apiDeleteCompose,
|
||||
apiDeployCompose,
|
||||
apiFetchServices,
|
||||
apiFindCompose,
|
||||
apiRandomizeCompose,
|
||||
apiRedeployCompose,
|
||||
apiUpdateCompose,
|
||||
compose as composeTable,
|
||||
} from "@/server/db/schema";
|
||||
@@ -367,7 +369,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.input(apiDeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
@@ -382,10 +384,10 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Manual deployment",
|
||||
titleLog: input.title || "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: "",
|
||||
descriptionLog: input.description || "",
|
||||
server: !!compose.serverId,
|
||||
};
|
||||
|
||||
@@ -404,7 +406,7 @@ export const composeRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.input(apiRedeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
@@ -418,10 +420,10 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Rebuild deployment",
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: "",
|
||||
descriptionLog: input.description || "",
|
||||
server: !!compose.serverId,
|
||||
};
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createDiscordNotification,
|
||||
createEmailNotification,
|
||||
createGotifyNotification,
|
||||
createNtfyNotification,
|
||||
createSlackNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
@@ -10,12 +11,14 @@ import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendNtfyNotification,
|
||||
sendServerThresholdNotifications,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
updateDiscordNotification,
|
||||
updateEmailNotification,
|
||||
updateGotifyNotification,
|
||||
updateNtfyNotification,
|
||||
updateSlackNotification,
|
||||
updateTelegramNotification,
|
||||
} from "@dokploy/server";
|
||||
@@ -33,17 +36,20 @@ import {
|
||||
apiCreateDiscord,
|
||||
apiCreateEmail,
|
||||
apiCreateGotify,
|
||||
apiCreateNtfy,
|
||||
apiCreateSlack,
|
||||
apiCreateTelegram,
|
||||
apiFindOneNotification,
|
||||
apiTestDiscordConnection,
|
||||
apiTestEmailConnection,
|
||||
apiTestGotifyConnection,
|
||||
apiTestNtfyConnection,
|
||||
apiTestSlackConnection,
|
||||
apiTestTelegramConnection,
|
||||
apiUpdateDiscord,
|
||||
apiUpdateEmail,
|
||||
apiUpdateGotify,
|
||||
apiUpdateNtfy,
|
||||
apiUpdateSlack,
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
@@ -321,6 +327,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
discord: true,
|
||||
email: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
},
|
||||
orderBy: desc(notifications.createdAt),
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -446,6 +453,64 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createNtfy: adminProcedure
|
||||
.input(apiCreateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createNtfyNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateNtfy: adminProcedure
|
||||
.input(apiUpdateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
notification.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateNtfyNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testNtfyConnection: adminProcedure
|
||||
.input(apiTestNtfyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await sendNtfyNotification(
|
||||
input,
|
||||
"Test Notification",
|
||||
"",
|
||||
"view, visit Dokploy on Github, https://github.com/dokploy/dokploy, clear=true;",
|
||||
"Hi, From Dokploy 👋",
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error testing the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
|
||||
Reference in New Issue
Block a user