mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into 57-dokploy-api-or-cli
This commit is contained in:
commit
ae4226531e
1
.gitignore
vendored
1
.gitignore
vendored
@ -55,3 +55,4 @@ yarn-error.log*
|
||||
|
||||
*.lockb
|
||||
*.rdb
|
||||
.idea
|
||||
|
@ -55,5 +55,5 @@ Tested Systems:
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
|
@ -183,8 +183,7 @@ export const AddPort = ({
|
||||
<SelectValue placeholder="Select a protocol" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent defaultValue={"none"}>
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
<SelectContent>
|
||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||
</SelectContent>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@ -19,8 +19,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@ -33,6 +34,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveEnvironment.useMutation();
|
||||
|
||||
@ -72,15 +74,50 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
toast.error("Error to add environment");
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
@ -97,6 +134,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
@ -111,7 +149,12 @@ PORT=3000
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
@ -51,18 +52,14 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application Deploying....");
|
||||
toast.success("Deploying Application....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@ -20,6 +20,8 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@ -32,6 +34,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
@ -71,14 +74,50 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
@ -95,6 +134,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
@ -109,7 +149,12 @@ PORT=3000
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -49,18 +49,14 @@ export const DeployCompose = ({ composeId }: Props) => {
|
||||
composeStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Deploying....");
|
||||
toast.success("Deploying Compose....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
|
@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Root Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databaseRootPassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||
/>
|
||||
|
@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||
/>
|
||||
|
@ -50,17 +50,13 @@ export const DeployMysql = ({ mysqlId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Root Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databaseRootPassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||
/>
|
||||
|
@ -50,17 +50,13 @@ export const DeployPostgres = ({ postgresId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
disabled
|
||||
<ToggleVisibilityInput
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||
/>
|
||||
|
@ -22,16 +22,26 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Folder } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
const AddTemplateSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "App name is required",
|
||||
})
|
||||
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||
message:
|
||||
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
@ -39,10 +49,13 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddApplication = ({ projectId }: Props) => {
|
||||
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.application.create.useMutation();
|
||||
@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
const form = useForm<AddTemplate>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
appName: `${slug}-`,
|
||||
description: "",
|
||||
},
|
||||
resolver: zodResolver(AddTemplateSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: AddTemplate) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
description: data.description,
|
||||
projectId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Created");
|
||||
form.reset();
|
||||
setVisible(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
toast.error("Error to create the service");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
@ -95,29 +108,46 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Frontend" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Frontend"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value?.trim() || "";
|
||||
form.setValue("appName", `${slug}-${val}`);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AppName</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
@ -136,47 +166,6 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="buildType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dockerfile" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Dockerfile
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="nixpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Nixpacks</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="heroku_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Heroku Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
|
@ -34,12 +34,22 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
const AddComposeSchema = z.object({
|
||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "App name is required",
|
||||
})
|
||||
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||
message:
|
||||
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
@ -47,11 +57,12 @@ type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddCompose = ({ projectId }: Props) => {
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const slug = slugify(projectName);
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.create.useMutation();
|
||||
|
||||
@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
||||
name: "",
|
||||
description: "",
|
||||
composeType: "docker-compose",
|
||||
appName: `${slug}-`,
|
||||
},
|
||||
resolver: zodResolver(AddComposeSchema),
|
||||
});
|
||||
@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
||||
description: data.description,
|
||||
projectId,
|
||||
composeType: data.composeType,
|
||||
appName: data.appName,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Frontend" {...field} />
|
||||
<Input
|
||||
placeholder="Frontend"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value?.trim() || "";
|
||||
form.setValue("appName", `${slug}-${val}`);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AppName</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composeType"
|
||||
|
@ -27,10 +27,11 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Database } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Database, AlertTriangle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@ -58,6 +59,15 @@ const databasesUserDefaultPlaceholder: Record<
|
||||
|
||||
const baseDatabaseSchema = z.object({
|
||||
name: z.string().min(1, "Name required"),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "App name is required",
|
||||
})
|
||||
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||
message:
|
||||
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string(),
|
||||
description: z.string().nullable(),
|
||||
@ -101,30 +111,52 @@ const mySchema = z.discriminatedUnion("type", [
|
||||
.merge(baseDatabaseSchema),
|
||||
]);
|
||||
|
||||
const databasesMap = {
|
||||
postgres: {
|
||||
icon: <PostgresqlIcon />,
|
||||
label: "PostgreSQL",
|
||||
},
|
||||
mongo: {
|
||||
icon: <MongodbIcon />,
|
||||
label: "MongoDB",
|
||||
},
|
||||
mariadb: {
|
||||
icon: <MariadbIcon />,
|
||||
label: "MariaDB",
|
||||
},
|
||||
mysql: {
|
||||
icon: <MysqlIcon />,
|
||||
label: "MySQL",
|
||||
},
|
||||
redis: {
|
||||
icon: <RedisIcon />,
|
||||
label: "Redis",
|
||||
},
|
||||
};
|
||||
|
||||
type AddDatabase = z.infer<typeof mySchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddDatabase = ({ projectId }: Props) => {
|
||||
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation();
|
||||
|
||||
const { mutateAsync: createMongo } = api.mongo.create.useMutation();
|
||||
|
||||
const { mutateAsync: createRedis } = api.redis.create.useMutation();
|
||||
|
||||
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
|
||||
|
||||
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
const redisMutation = api.redis.create.useMutation();
|
||||
const mariadbMutation = api.mariadb.create.useMutation();
|
||||
const mysqlMutation = api.mysql.create.useMutation();
|
||||
|
||||
const form = useForm<AddDatabase>({
|
||||
defaultValues: {
|
||||
type: "postgres",
|
||||
dockerImage: "",
|
||||
name: "",
|
||||
appName: `${slug}-`,
|
||||
databasePassword: "",
|
||||
description: "",
|
||||
databaseName: "",
|
||||
@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
const type = form.watch("type");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
type: "postgres",
|
||||
dockerImage: "",
|
||||
name: "",
|
||||
databasePassword: "",
|
||||
description: "",
|
||||
databaseName: "",
|
||||
databaseUser: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
const activeMutation = {
|
||||
postgres: postgresMutation,
|
||||
mongo: mongoMutation,
|
||||
redis: redisMutation,
|
||||
mariadb: mariadbMutation,
|
||||
mysql: mysqlMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: AddDatabase) => {
|
||||
const defaultDockerImage =
|
||||
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
||||
|
||||
let promise: Promise<unknown> | null = null;
|
||||
const commonParams = {
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
dockerImage: defaultDockerImage,
|
||||
projectId,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
if (data.type === "postgres") {
|
||||
promise = createPostgresql({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = postgresMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
projectId,
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "mongo") {
|
||||
promise = createMongo({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = mongoMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
projectId,
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "redis") {
|
||||
promise = createRedis({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = redisMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
projectId,
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "mariadb") {
|
||||
promise = createMariadb({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = mariadbMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
projectId,
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "mysql") {
|
||||
promise = createMysql({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = mysqlMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
projectId,
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
description: data.description,
|
||||
});
|
||||
}
|
||||
|
||||
@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
await promise
|
||||
.then(async () => {
|
||||
toast.success("Database Created");
|
||||
form.reset({
|
||||
type: "postgres",
|
||||
dockerImage: "",
|
||||
name: "",
|
||||
appName: `${projectName}-`,
|
||||
databasePassword: "",
|
||||
description: "",
|
||||
databaseName: "",
|
||||
databaseUser: "",
|
||||
});
|
||||
setVisible(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
@ -230,18 +262,10 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
<span>Database</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Databases</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* {isError && (
|
||||
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@ -264,99 +288,40 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
defaultValue={field.value}
|
||||
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<FormItem className="flex w-full items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="postgres"
|
||||
id="postgres"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="postgres"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<PostgresqlIcon />
|
||||
Postgresql
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="mysql"
|
||||
id="mysql"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="mysql"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<MysqlIcon />
|
||||
Mysql
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="mariadb"
|
||||
id="mariadb"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="mariadb"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<MariadbIcon />
|
||||
Mariadb
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="mongo"
|
||||
id="mongo"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="mongo"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<MongodbIcon />
|
||||
Mongo
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="redis"
|
||||
id="redis"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="redis"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<RedisIcon />
|
||||
Redis
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
{Object.entries(databasesMap).map(([key, value]) => (
|
||||
<FormItem
|
||||
key={key}
|
||||
className="flex w-full items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value={key}
|
||||
id={key}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
{value.icon}
|
||||
{value.label}
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{activeMutation[field.value].isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{activeMutation[field.value].error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name" {...field} />
|
||||
<Input
|
||||
placeholder="Name"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value?.trim() || "";
|
||||
form.setValue("appName", `${slug}-${val}`);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AppName</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
@ -50,17 +50,13 @@ export const DeployRedis = ({ redisId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database...");
|
||||
await refetch();
|
||||
await deploy({
|
||||
redisId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
redisId: string;
|
||||
@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
disabled
|
||||
<ToggleVisibilityInput
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
||||
/>
|
||||
|
26
components/shared/toggle-visibility-input.tsx
Normal file
26
components/shared/toggle-visibility-input.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { useState } from "react";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { Input, type InputProps } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setIsPasswordVisible((prevVisibility) => !prevVisibility);
|
||||
};
|
||||
|
||||
const inputType = isPasswordVisible ? "text" : "password";
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Input type={inputType} {...props} />
|
||||
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||
{inputType === "password" ? (
|
||||
<EyeIcon className="size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeOffIcon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
15
lib/slug.ts
Normal file
15
lib/slug.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import slug from "slugify";
|
||||
|
||||
export const slugify = (text: string | undefined) => {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
||||
|
||||
return slug(cleanedText, {
|
||||
lower: true,
|
||||
trim: true,
|
||||
strict: true,
|
||||
});
|
||||
};
|
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.2.0",
|
||||
"version": "v0.2.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
@ -31,11 +31,11 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/language":"^6.10.1",
|
||||
"@aws-sdk/client-s3": "3.515.0",
|
||||
"@codemirror/legacy-modes":"6.4.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
@ -76,6 +76,7 @@
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
@ -105,6 +106,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"recharts": "^2.12.3",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.4.0",
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
@ -113,9 +115,7 @@
|
||||
"use-resize-observer": "9.1.0",
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"copy-webpack-plugin": "^12.0.2"
|
||||
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.7.1",
|
||||
|
@ -210,9 +210,12 @@ const Project = (
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<AddApplication projectId={projectId} />
|
||||
<AddDatabase projectId={projectId} />
|
||||
<AddCompose projectId={projectId} />
|
||||
<AddApplication
|
||||
projectId={projectId}
|
||||
projectName={data?.name}
|
||||
/>
|
||||
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||
<AddTemplate projectId={projectId} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
@ -230,6 +230,9 @@ dependencies:
|
||||
recharts:
|
||||
specifier: ^2.12.3
|
||||
version: 2.12.3(react-dom@18.2.0)(react@18.2.0)
|
||||
slugify:
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
sonner:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -8015,6 +8018,11 @@ packages:
|
||||
engines: {node: '>=14.16'}
|
||||
dev: false
|
||||
|
||||
/slugify@1.6.6:
|
||||
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dev: false
|
||||
|
||||
/sonner@1.4.3(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==}
|
||||
peerDependencies:
|
||||
|
@ -66,9 +66,10 @@ export const applicationRouter = createTRPCRouter({
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||
}
|
||||
|
||||
return newApplication;
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the application",
|
||||
|
@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
|
||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
|
||||
import {
|
||||
generatePassword,
|
||||
loadTemplateModule,
|
||||
readComposeFile,
|
||||
} from "@/templates/utils";
|
||||
import { findAdmin } from "../services/admin";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
||||
import { findProjectById } from "../services/project";
|
||||
import { createMount } from "../services/mount";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import { templates } from "@/templates/templates";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
||||
const projectName = slugify(`${project.name} ${input.id}`);
|
||||
const { envs, mounts } = generate({
|
||||
serverIp: admin.serverIp,
|
||||
projectName: projectName,
|
||||
@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
|
||||
env: envs.join("\n"),
|
||||
name: input.id,
|
||||
sourceType: "raw",
|
||||
appName: `${projectName}-${generatePassword(6)}`,
|
||||
});
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
|
@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mariadb database",
|
||||
|
@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mongo database",
|
||||
|
@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mysql database",
|
||||
|
@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting postgresql database",
|
||||
|
@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { manageRegistry } from "@/server/utils/traefik/registry";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { docker } from "@/server/constants";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export const registryRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({
|
||||
.input(apiTestRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const result = await docker.checkAuth({
|
||||
username: input.username,
|
||||
password: input.password,
|
||||
serveraddress: input.registryUrl,
|
||||
});
|
||||
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log("Error Registry:", error);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
@ -15,11 +15,23 @@ import { findAdmin } from "./admin";
|
||||
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
||||
import { docker } from "@/server/constants";
|
||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
input: typeof apiCreateApplication._type,
|
||||
) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Application with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const newApplication = await tx
|
||||
.insert(applications)
|
||||
|
@ -13,10 +13,21 @@ import { join } from "node:path";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
@ -39,6 +50,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
export const createComposeByTemplate = async (
|
||||
input: typeof compose.$inferInsert,
|
||||
) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
|
@ -46,9 +46,7 @@ export const getContainers = async () => {
|
||||
.filter((container) => !container.name.includes("dokploy"));
|
||||
|
||||
return containers;
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getConfig = async (containerId: string) => {
|
||||
@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getContainersByAppNameMatch = async (appName: string) => {
|
||||
@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Mariadb = typeof mariadb.$inferSelect;
|
||||
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newMariadb = await db
|
||||
.insert(mariadb)
|
||||
.values({
|
||||
|
@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newMongo = await db
|
||||
.insert(mongo)
|
||||
.values({
|
||||
|
@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type MySql = typeof mysql.$inferSelect;
|
||||
|
||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newMysql = await db
|
||||
.insert(mysql)
|
||||
.values({
|
||||
|
@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Postgres = typeof postgres.$inferSelect;
|
||||
|
||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPostgres = await db
|
||||
.insert(postgres)
|
||||
.values({
|
||||
|
@ -1,5 +1,14 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateProject, projects } from "@/server/db/schema";
|
||||
import {
|
||||
type apiCreateProject,
|
||||
applications,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
projects,
|
||||
redis,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findAdmin } from "./admin";
|
||||
@ -75,12 +84,40 @@ export const updateProjectById = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const slugifyProjectName = (projectName: string): string => {
|
||||
return projectName
|
||||
.toLowerCase()
|
||||
.replace(/[0-9]/g, "")
|
||||
.replace(/[^a-z\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
export const validUniqueServerAppName = async (appName: string) => {
|
||||
const query = await db.query.projects.findMany({
|
||||
with: {
|
||||
applications: {
|
||||
where: eq(applications.appName, appName),
|
||||
},
|
||||
mariadb: {
|
||||
where: eq(mariadb.appName, appName),
|
||||
},
|
||||
mongo: {
|
||||
where: eq(mongo.appName, appName),
|
||||
},
|
||||
mysql: {
|
||||
where: eq(mysql.appName, appName),
|
||||
},
|
||||
postgres: {
|
||||
where: eq(postgres.appName, appName),
|
||||
},
|
||||
redis: {
|
||||
where: eq(redis.appName, appName),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Filter out items with non-empty fields
|
||||
const nonEmptyProjects = query.filter(
|
||||
(project) =>
|
||||
project.applications.length > 0 ||
|
||||
project.mariadb.length > 0 ||
|
||||
project.mongo.length > 0 ||
|
||||
project.mysql.length > 0 ||
|
||||
project.postgres.length > 0 ||
|
||||
project.redis.length > 0,
|
||||
);
|
||||
|
||||
return nonEmptyProjects.length === 0;
|
||||
};
|
||||
|
@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Redis = typeof redis.$inferSelect;
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newRedis = await db
|
||||
.insert(redis)
|
||||
.values({
|
||||
|
@ -9,28 +9,35 @@ import {
|
||||
} from "@/server/utils/traefik/registry";
|
||||
import { removeService } from "@/server/utils/docker/utils";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export type Registry = typeof registry.$inferSelect;
|
||||
|
||||
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const newRegistry = await db
|
||||
.insert(registry)
|
||||
.values({
|
||||
...input,
|
||||
adminId: admin.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
return await db.transaction(async (tx) => {
|
||||
const newRegistry = await tx
|
||||
.insert(registry)
|
||||
.values({
|
||||
...input,
|
||||
adminId: admin.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newRegistry) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting registry",
|
||||
});
|
||||
}
|
||||
return newRegistry;
|
||||
if (!newRegistry) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting registry",
|
||||
});
|
||||
}
|
||||
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
|
||||
return newRegistry;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeRegistry = async (registryId: string) => {
|
||||
@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => {
|
||||
await removeService("dokploy-registry");
|
||||
}
|
||||
|
||||
await execAsync(`docker logout ${response.registryUrl}`);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { generateAppName } from "./utils";
|
||||
import { registry } from "./registry";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||
|
||||
@ -307,11 +308,17 @@ const createSchema = createInsertSchema(applications, {
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
});
|
||||
export const apiCreateApplication = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"),
|
||||
}));
|
||||
|
||||
export const apiFindOneApplication = createSchema
|
||||
.pick({
|
||||
|
@ -8,6 +8,7 @@ import { deployments } from "./deployment";
|
||||
import { generateAppName } from "./utils";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { mounts } from "./mount";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
@ -74,12 +75,19 @@ const createSchema = createInsertSchema(compose, {
|
||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateCompose = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
composeType: true,
|
||||
});
|
||||
export const apiCreateCompose = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
composeType: true,
|
||||
appName: true,
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName:
|
||||
`${data.appName}-${generatePassword(6)}` || generateAppName("compose"),
|
||||
}));
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
.pick({
|
||||
|
@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const mariadb = pgTable("mariadb", {
|
||||
mariadbId: text("mariadbId")
|
||||
@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
export const apiCreateMariaDB = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
databaseRootPassword: true,
|
||||
projectId: true,
|
||||
@ -87,7 +89,12 @@ export const apiCreateMariaDB = createSchema
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName:
|
||||
`${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"),
|
||||
}));
|
||||
|
||||
export const apiFindOneMariaDB = createSchema
|
||||
.pick({
|
||||
|
@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const mongo = pgTable("mongo", {
|
||||
mongoId: text("mongoId")
|
||||
@ -73,13 +74,19 @@ const createSchema = createInsertSchema(mongo, {
|
||||
export const apiCreateMongo = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName:
|
||||
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
|
||||
}));
|
||||
|
||||
export const apiFindOneMongo = createSchema
|
||||
.pick({
|
||||
|
@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const mysql = pgTable("mysql", {
|
||||
mysqlId: text("mysqlId")
|
||||
@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
export const apiCreateMySql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
@ -85,7 +87,12 @@ export const apiCreateMySql = createSchema
|
||||
databasePassword: true,
|
||||
databaseRootPassword: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName:
|
||||
`${data.appName}-${generatePassword(6)}` || generateAppName("mysql"),
|
||||
}));
|
||||
|
||||
export const apiFindOneMySql = createSchema
|
||||
.pick({
|
||||
|
@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const postgres = pgTable("postgres", {
|
||||
postgresId: text("postgresId")
|
||||
@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
|
||||
export const apiCreatePostgres = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
@ -81,7 +83,12 @@ export const apiCreatePostgres = createSchema
|
||||
projectId: true,
|
||||
description: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName:
|
||||
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
|
||||
}));
|
||||
|
||||
export const apiFindOnePostgres = createSchema
|
||||
.pick({
|
||||
|
@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const redis = pgTable("redis", {
|
||||
redisId: text("redisId")
|
||||
@ -69,13 +70,18 @@ const createSchema = createInsertSchema(redis, {
|
||||
export const apiCreateRedis = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
})
|
||||
|
||||
.required();
|
||||
.required()
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
appName:
|
||||
`${data.appName}-${generatePassword(6)}` || generateAppName("redis"),
|
||||
}));
|
||||
|
||||
export const apiFindOneRedis = createSchema
|
||||
.pick({
|
||||
|
@ -7,6 +7,10 @@ import type { MainTraefikConfig } from "../utils/traefik/types";
|
||||
import type { FileConfig } from "../utils/traefik/file-types";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
|
||||
const TRAEFIK_SSL_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
|
||||
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
|
||||
|
||||
export const initializeTraefik = async () => {
|
||||
const imageName = "traefik:v2.5";
|
||||
const containerName = "dokploy-traefik";
|
||||
@ -47,12 +51,12 @@ export const initializeTraefik = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 443,
|
||||
PublishedPort: 443,
|
||||
PublishedPort: TRAEFIK_SSL_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: 80,
|
||||
PublishedPort: TRAEFIK_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
@ -146,10 +150,10 @@ export const createDefaultTraefikConfig = () => {
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: ":80",
|
||||
address: `:${TRAEFIK_PORT}`,
|
||||
},
|
||||
websecure: {
|
||||
address: ":443",
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
|
@ -108,7 +108,12 @@ const createEnvFile = (compose: ComposeNested) => {
|
||||
join(COMPOSE_PATH, appName, "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
let envContent = env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
|
@ -148,7 +148,6 @@ export const mechanizeDockerContainer = async (
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
@ -3,47 +3,47 @@ import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
||||
import { pullImage } from "../docker/utils";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
}
|
||||
|
||||
export const buildDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
): Promise<void> => {
|
||||
const { buildType, dockerImage, username, password } = application;
|
||||
const authConfig: Partial<RegistryAuth> = {
|
||||
username: username || "",
|
||||
password: password || "",
|
||||
};
|
||||
const { buildType, dockerImage, username, password } = application;
|
||||
const authConfig: Partial<RegistryAuth> = {
|
||||
username: username || "",
|
||||
password: password || "",
|
||||
};
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
|
||||
writeStream.write(`\nBuild ${buildType}\n`);
|
||||
writeStream.write(`\nBuild ${buildType}\n`);
|
||||
|
||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
|
||||
await pullImage(
|
||||
dockerImage,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(`${data.status}\n`);
|
||||
}
|
||||
},
|
||||
authConfig,
|
||||
);
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("\nDocker Deployed: ✅\n");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
await pullImage(
|
||||
dockerImage,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(`${data.status}\n`);
|
||||
}
|
||||
},
|
||||
authConfig,
|
||||
);
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("\nDocker Deployed: ✅\n");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
11
utils/api.ts
11
utils/api.ts
@ -35,17 +35,6 @@ export const api = createTRPCNext<AppRouter>({
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
}),
|
||||
// createWSClient({
|
||||
// url: `ws://localhost:3000`,
|
||||
// }),
|
||||
// loggerLink({
|
||||
// enabled: (opts) =>
|
||||
// process.env.NODE_ENV === "development" ||
|
||||
// (opts.direction === "down" && opts.result instanceof Error),
|
||||
// }),
|
||||
// httpBatchLink({
|
||||
// url: `${getBaseUrl()}/api/trpc`,
|
||||
// }),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user