Merge pull request #785 from Dokploy/canary

v0.13.1
This commit is contained in:
Mauricio Siu
2024-11-28 23:44:33 -06:00
committed by GitHub
79 changed files with 9791 additions and 787 deletions

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm commitlint --edit $1

View File

@@ -1,6 +0,0 @@
// Skip Husky install in production and CI
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
process.exit(0);
}
const husky = (await import("husky")).default;
console.log(husky());

View File

@@ -1 +0,0 @@
pnpm lint-staged

View File

@@ -4,9 +4,6 @@ on:
pull_request: pull_request:
branches: [main, canary] branches: [main, canary]
env:
HUSKY: 0
jobs: jobs:
lint-and-typecheck: lint-and-typecheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -18,8 +15,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build
- run: pnpm biome ci
- run: pnpm typecheck - run: pnpm typecheck
build-and-test: build-and-test:
@@ -46,5 +42,5 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build
- run: pnpm test - run: pnpm test

View File

@@ -1 +0,0 @@
npx commitlint --edit "$1"

View File

@@ -1,6 +0,0 @@
// Skip Husky install in production and CI
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
process.exit(0);
}
const husky = (await import("husky")).default;
console.log(husky());

View File

@@ -1,2 +0,0 @@
pnpm run check
git add .

View File

@@ -30,6 +30,7 @@ const baseApp: ApplicationNested = {
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
serverId: "", serverId: "",
registryUrl: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",
project: { project: {

View File

@@ -12,6 +12,7 @@ const baseApp: ApplicationNested = {
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",
registryUrl: "",
buildArgs: null, buildArgs: null,
project: { project: {
env: "", env: "",

View File

@@ -1,63 +1,143 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deleteApplicationSchema = z.object({
projectName: z.string().min(1, {
message: "Application name is required",
}),
});
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
interface Props { interface Props {
applicationId: string; applicationId: string;
} }
export const DeleteApplication = ({ applicationId }: Props) => { export const DeleteApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.application.delete.useMutation(); const { mutateAsync, isLoading } = api.application.delete.useMutation();
const { data } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeleteApplication>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteApplicationSchema),
});
const onSubmit = async (formData: DeleteApplication) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({
applicationId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Application deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the application");
});
} else {
form.setError("projectName", {
message: "Project name does not match",
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
application application. If you are sure please enter the application name to
</AlertDialogDescription> delete this application.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
applicationId, id="hook-form-delete-application"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
control={form.control}
toast.success("Application delete succesfully"); name="projectName"
}) render={({ field }) => (
.catch(() => { <FormItem>
toast.error("Error to delete Application"); <FormLabel>
}); To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter application name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-application"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -202,7 +202,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
@@ -281,7 +280,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -21,6 +21,7 @@ const DockerProviderSchema = z.object({
}), }),
username: z.string().optional(), username: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
registryURL: z.string().optional(),
}); });
type DockerProvider = z.infer<typeof DockerProviderSchema>; type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -33,12 +34,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId }); const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync } = api.application.saveDockerProvider.useMutation(); const { mutateAsync } = api.application.saveDockerProvider.useMutation();
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: { defaultValues: {
dockerImage: "", dockerImage: "",
password: "", password: "",
username: "", username: "",
registryURL: "",
}, },
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
}); });
@@ -49,6 +50,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
dockerImage: data.dockerImage || "", dockerImage: data.dockerImage || "",
password: data.password || "", password: data.password || "",
username: data.username || "", username: data.username || "",
registryURL: data.registryUrl || "",
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -59,6 +61,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
password: values.password || null, password: values.password || null,
applicationId, applicationId,
username: values.username || null, username: values.username || null,
registryUrl: values.registryURL || null,
}) })
.then(async () => { .then(async () => {
toast.success("Docker Provider Saved"); toast.success("Docker Provider Saved");
@@ -76,7 +79,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
className="flex flex-col gap-4" className="flex flex-col gap-4"
> >
<div className="grid md:grid-cols-2 gap-4 "> <div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4"> <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="dockerImage" name="dockerImage"
@@ -91,6 +94,19 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
)} )}
/> />
</div> </div>
<FormField
control={form.control}
name="registryURL"
render={({ field }) => (
<FormItem>
<FormLabel>Registry URL</FormLabel>
<FormControl>
<Input placeholder="Registry URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4"> <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -193,7 +193,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
@@ -272,7 +271,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -209,7 +209,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
@@ -297,7 +296,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -1,63 +1,141 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deleteComposeSchema = z.object({
projectName: z.string().min(1, {
message: "Compose name is required",
}),
});
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
interface Props { interface Props {
composeId: string; composeId: string;
} }
export const DeleteCompose = ({ composeId }: Props) => { export const DeleteCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.compose.delete.useMutation(); const { mutateAsync, isLoading } = api.compose.delete.useMutation();
const { data } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeleteCompose>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteComposeSchema),
});
const onSubmit = async (formData: DeleteCompose) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ composeId })
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
toast.success("Compose deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the compose");
});
} else {
form.setError("projectName", {
message: `Project name must match "${expectedName}"`,
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
compose and all its services. compose. If you are sure please enter the compose name to delete
</AlertDialogDescription> this compose.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
composeId, id="hook-form-delete-compose"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
control={form.control}
toast.success("Compose delete succesfully"); name="projectName"
}) render={({ field }) => (
.catch(() => { <FormItem>
toast.error("Error to delete the compose"); <FormLabel>
}); To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>{" "}
<FormControl>
<Input
placeholder="Enter compose name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-compose"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -204,7 +204,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
@@ -283,7 +282,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -195,7 +195,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
@@ -274,7 +273,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -211,7 +211,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
@@ -299,7 +298,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -160,7 +160,6 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -144,7 +144,6 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
<FormControl> <FormControl>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between !bg-input",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",

View File

@@ -1,62 +1,140 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deleteMariadbSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMariadb = z.infer<typeof deleteMariadbSchema>;
interface Props { interface Props {
mariadbId: string; mariadbId: string;
} }
export const DeleteMariadb = ({ mariadbId }: Props) => { export const DeleteMariadb = ({ mariadbId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation(); const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
const { data } = api.mariadb.one.useQuery(
{ mariadbId },
{ enabled: !!mariadbId },
);
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeleteMariadb>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMariadbSchema),
});
const onSubmit = async (formData: DeleteMariadb) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mariadbId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
database database. If you are sure please enter the database name to delete
</AlertDialogDescription> this database.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
mariadbId, id="hook-form-delete-mariadb"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
toast.success("Database delete succesfully"); control={form.control}
}) name="projectName"
.catch(() => { render={({ field }) => (
toast.error("Error to delete the database"); <FormItem>
}); <FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mariadb"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -1,62 +1,139 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deleteMongoSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMongo = z.infer<typeof deleteMongoSchema>;
interface Props { interface Props {
mongoId: string; mongoId: string;
} }
// commen
export const DeleteMongo = ({ mongoId }: Props) => { export const DeleteMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mongo.remove.useMutation(); const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId });
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeleteMongo>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMongoSchema),
});
const onSubmit = async (formData: DeleteMongo) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mongoId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
database database. If you are sure please enter the database name to delete
</AlertDialogDescription> this database.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
mongoId, id="hook-form-delete-mongo"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
toast.success("Database delete succesfully"); control={form.control}
}) name="projectName"
.catch(() => { render={({ field }) => (
toast.error("Error to delete the database"); <FormItem>
}); <FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mongo"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -1,62 +1,138 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deleteMysqlSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMysql = z.infer<typeof deleteMysqlSchema>;
interface Props { interface Props {
mysqlId: string; mysqlId: string;
} }
export const DeleteMysql = ({ mysqlId }: Props) => { export const DeleteMysql = ({ mysqlId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mysql.remove.useMutation(); const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId });
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeleteMysql>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMysqlSchema),
});
const onSubmit = async (formData: DeleteMysql) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mysqlId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
database database. If you are sure please enter the database name to delete
</AlertDialogDescription> this database.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
mysqlId, id="hook-form-delete-mysql"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
toast.success("Database delete succesfully"); control={form.control}
}) name="projectName"
.catch(() => { render={({ field }) => (
toast.error("Error to delete the database"); <FormItem>
}); <FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mysql"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -86,14 +86,12 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
)} )}
{mount.type === "file" && ( {mount.type === "file" && (
<> <div className="flex flex-col gap-1">
<div className="flex flex-col gap-1"> <span className="font-medium">Content</span>
<span className="font-medium">Content</span> <span className="text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground"> {mount.content}
{mount.content} </span>
</span> </div>
</div>
</>
)} )}
{mount.type === "bind" && ( {mount.type === "bind" && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">

View File

@@ -1,62 +1,141 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deletePostgresSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeletePostgres = z.infer<typeof deletePostgresSchema>;
interface Props { interface Props {
postgresId: string; postgresId: string;
} }
export const DeletePostgres = ({ postgresId }: Props) => { export const DeletePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.postgres.remove.useMutation(); const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
const { data } = api.postgres.one.useQuery(
{ postgresId },
{ enabled: !!postgresId },
);
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeletePostgres>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deletePostgresSchema),
});
const onSubmit = async (formData: DeletePostgres) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ postgresId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
database database. If you are sure please enter the database name to delete
</AlertDialogDescription> this database.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
postgresId, id="hook-form-delete-postgres"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
toast.success("Database delete succesfully"); control={form.control}
}) name="projectName"
.catch(() => { render={({ field }) => (
toast.error("Error to delete the database"); <FormItem>
}); <FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-postgres"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -127,7 +127,6 @@ export const AddTemplate = ({ projectId }: Props) => {
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
role="combobox"
className={cn( className={cn(
"md:max-w-[15rem] w-full justify-between !bg-input", "md:max-w-[15rem] w-full justify-between !bg-input",
)} )}

View File

@@ -19,11 +19,9 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, FileIcon, SquarePen } from "lucide-react"; import { FileIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -37,9 +35,10 @@ type UpdateProject = z.infer<typeof updateProjectSchema>;
interface Props { interface Props {
projectId: string; projectId: string;
children?: React.ReactNode;
} }
export const AddEnv = ({ projectId }: Props) => { export const ProjectEnviroment = ({ projectId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } = const { mutateAsync, error, isError, isLoading } =
@@ -53,7 +52,6 @@ export const AddEnv = ({ projectId }: Props) => {
}, },
); );
console.log(data);
const form = useForm<UpdateProject>({ const form = useForm<UpdateProject>({
defaultValues: { defaultValues: {
env: data?.env ?? "", env: data?.env ?? "",
@@ -86,34 +84,29 @@ export const AddEnv = ({ projectId }: Props) => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<DropdownMenuItem {children ?? (
className="w-full cursor-pointer space-x-3" <DropdownMenuItem
onSelect={(e) => e.preventDefault()} className="w-full cursor-pointer space-x-3"
> onSelect={(e) => e.preventDefault()}
<FileIcon className="size-4" /> >
<span>Add Env</span> <FileIcon className="size-4" />
</DropdownMenuItem> <span>Project Enviroment</span>
</DropdownMenuItem>
)}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl"> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Modify Shared Env</DialogTitle> <DialogTitle>Project Enviroment</DialogTitle>
<DialogDescription>Update the env variables</DialogDescription> <DialogDescription>
Update the env Environment variables that are accessible to all
services of this project. Use this syntax to reference project-level
variables in your service environments:
</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<AlertBlock type="info"> <AlertBlock type="info">
To use a shared env, in one of your services, you need to use like Use this syntax to reference project-level variables in your service
this: Let's say you have a shared env ENVIROMENT="development" and you environments: <code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
want to use it in your service, you need to use like this:
<ul>
<li>
<code>ENVIRONMENT=${"{{project.ENVIRONMENT}}"}</code>
</li>
<li>
<code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
</li>
</ul>{" "}
This allows the service to inherit and use the shared variables from
the project level, ensuring consistency across services.
</AlertBlock> </AlertBlock>
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -25,7 +25,6 @@ import { api } from "@/utils/api";
import { import {
AlertTriangle, AlertTriangle,
BookIcon, BookIcon,
CircuitBoard,
ExternalLink, ExternalLink,
ExternalLinkIcon, ExternalLinkIcon,
FolderInput, FolderInput,
@@ -35,7 +34,7 @@ import {
import Link from "next/link"; import Link from "next/link";
import { Fragment } from "react"; import { Fragment } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AddEnv } from "./add-env"; import { ProjectEnviroment } from "./project-enviroment";
import { UpdateProject } from "./update"; import { UpdateProject } from "./update";
export const ShowProjects = () => { export const ShowProjects = () => {
@@ -192,7 +191,9 @@ export const ShowProjects = () => {
Actions Actions
</DropdownMenuLabel> </DropdownMenuLabel>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<AddEnv projectId={project.projectId} /> <ProjectEnviroment
projectId={project.projectId}
/>
</div> </div>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<UpdateProject projectId={project.projectId} /> <UpdateProject projectId={project.projectId} />

View File

@@ -1,62 +1,138 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const deleteRedisSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteRedis = z.infer<typeof deleteRedisSchema>;
interface Props { interface Props {
redisId: string; redisId: string;
} }
export const DeleteRedis = ({ redisId }: Props) => { export const DeleteRedis = ({ redisId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.redis.remove.useMutation(); const { mutateAsync, isLoading } = api.redis.remove.useMutation();
const { data } = api.redis.one.useQuery({ redisId }, { enabled: !!redisId });
const { push } = useRouter(); const { push } = useRouter();
const form = useForm<DeleteRedis>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteRedisSchema),
});
const onSubmit = async (formData: DeleteRedis) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ redisId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return ( return (
<AlertDialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}> <Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " /> <TrashIcon className="size-4 text-muted-foreground" />
</Button> </Button>
</AlertDialogTrigger> </DialogTrigger>
<AlertDialogContent> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <DialogTitle>Are you absolutely sure?</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This action cannot be undone. This will permanently delete the This action cannot be undone. This will permanently delete the
database database. If you are sure please enter the database name to delete
</AlertDialogDescription> this database.
</AlertDialogHeader> </DialogDescription>
<AlertDialogFooter> </DialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <div className="grid gap-4">
<AlertDialogAction <Form {...form}>
onClick={async () => { <form
await mutateAsync({ onSubmit={form.handleSubmit(onSubmit)}
redisId, id="hook-form-delete-redis"
}) className="grid w-full gap-4"
.then((data) => { >
push(`/dashboard/project/${data?.projectId}`); <FormField
toast.success("Database delete succesfully"); control={form.control}
}) name="projectName"
.catch(() => { render={({ field }) => (
toast.error("Error to delete the database"); <FormItem>
}); <FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}} }}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-redis"
type="submit"
variant="destructive"
> >
Confirm Confirm
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
); );
}; };

View File

@@ -37,9 +37,12 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], { theme: z.enum(["light", "dark", "system"], {
required_error: "Please select a theme.", required_error: "Please select a theme.",
}), }),
language: z.enum(["en", "pl", "ru", "de", "zh-Hant", "zh-Hans"], { language: z.enum(
required_error: "Please select a language.", ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
}), {
required_error: "Please select a language.",
},
),
}); });
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>; type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
@@ -176,9 +179,15 @@ export function AppearanceForm() {
{ label: "English", value: "en" }, { label: "English", value: "en" },
{ label: "Polski", value: "pl" }, { label: "Polski", value: "pl" },
{ label: "Русский", value: "ru" }, { label: "Русский", value: "ru" },
{ label: "Français", value: "fr" },
{ label: "Deutsch", value: "de" }, { label: "Deutsch", value: "de" },
{ label: "繁體中文", value: "zh-Hant" }, { label: "繁體中文", value: "zh-Hant" },
{ label: "简体中文", value: "zh-Hans" }, { label: "简体中文", value: "zh-Hans" },
{ label: "Türkçe", value: "tr" },
{
label: "Persian",
value: "fa",
},
].map((preset) => ( ].map((preset) => (
<SelectItem key={preset.label} value={preset.value}> <SelectItem key={preset.label} value={preset.value}>
{preset.label} {preset.label}

View File

@@ -89,18 +89,14 @@ export const ShowBilling = () => {
<div className="pb-5"> <div className="pb-5">
<Progress value={safePercentage} className="max-w-lg" /> <Progress value={safePercentage} className="max-w-lg" />
</div> </div>
{admin && ( {admin && admin.serversQuantity! <= servers?.length! && (
<> <div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
{admin.serversQuantity! <= servers?.length! && ( <AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center"> <span className="text-sm text-yellow-600 dark:text-yellow-400">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" /> You have reached the maximum number of servers you can create,
<span className="text-sm text-yellow-600 dark:text-yellow-400"> please upgrade your plan to add more servers.
You have reached the maximum number of servers you can </span>
create, please upgrade your plan to add more servers. </div>
</span>
</div>
)}
</>
)} )}
</div> </div>
)} )}
@@ -188,7 +184,6 @@ export const ShowBilling = () => {
</p> </p>
<ul <ul
role="list"
className={clsx( className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm", " mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200", featured ? "text-white" : "text-slate-200",

View File

@@ -28,11 +28,7 @@ export const ShowRegistry = () => {
</div> </div>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
{data && data?.length > 0 && ( {data && data?.length > 0 && <AddRegistry />}
<>
<AddRegistry />
</>
)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 pt-4 h-full"> <CardContent className="space-y-2 pt-4 h-full">

View File

@@ -34,9 +34,11 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({ const addDestination = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(), accessKeyId: z.string(),
secretAccessKey: z.string(), secretAccessKey: z.string(),
bucket: z.string(), bucket: z.string(),
@@ -58,6 +60,7 @@ export const AddDestination = () => {
api.destination.testConnection.useMutation(); api.destination.testConnection.useMutation();
const form = useForm<AddDestination>({ const form = useForm<AddDestination>({
defaultValues: { defaultValues: {
provider: "",
accessKeyId: "", accessKeyId: "",
bucket: "", bucket: "",
name: "", name: "",
@@ -73,6 +76,7 @@ export const AddDestination = () => {
const onSubmit = async (data: AddDestination) => { const onSubmit = async (data: AddDestination) => {
await mutateAsync({ await mutateAsync({
provider: data.provider || "",
accessKey: data.accessKeyId, accessKey: data.accessKeyId,
bucket: data.bucket, bucket: data.bucket,
endpoint: data.endpoint, endpoint: data.endpoint,
@@ -123,6 +127,40 @@ export const AddDestination = () => {
); );
}} }}
/> />
<FormField
control={form.control}
name="provider"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Provider</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a S3 Provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
{S3_PROVIDERS.map((s3Provider) => (
<SelectItem
key={s3Provider.key}
value={s3Provider.key}
>
{s3Provider.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField <FormField
control={form.control} control={form.control}
@@ -255,6 +293,7 @@ export const AddDestination = () => {
isLoading={isLoading} isLoading={isLoading}
onClick={async () => { onClick={async () => {
await testConnection({ await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"), accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"), bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"), endpoint: form.getValues("endpoint"),
@@ -283,6 +322,7 @@ export const AddDestination = () => {
variant="secondary" variant="secondary"
onClick={async () => { onClick={async () => {
await testConnection({ await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"), accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"), bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"), endpoint: form.getValues("endpoint"),

View File

@@ -0,0 +1,133 @@
export const S3_PROVIDERS: Array<{
key: string;
name: string;
}> = [
{
key: "AWS",
name: "Amazon Web Services (AWS) S3",
},
{
key: "Alibaba",
name: "Alibaba Cloud Object Storage System (OSS) formerly Aliyun",
},
{
key: "ArvanCloud",
name: "Arvan Cloud Object Storage (AOS)",
},
{
key: "Ceph",
name: "Ceph Object Storage",
},
{
key: "ChinaMobile",
name: "China Mobile Ecloud Elastic Object Storage (EOS)",
},
{
key: "Cloudflare",
name: "Cloudflare R2 Storage",
},
{
key: "DigitalOcean",
name: "DigitalOcean Spaces",
},
{
key: "Dreamhost",
name: "Dreamhost DreamObjects",
},
{
key: "GCS",
name: "Google Cloud Storage",
},
{
key: "HuaweiOBS",
name: "Huawei Object Storage Service",
},
{
key: "IBMCOS",
name: "IBM COS S3",
},
{
key: "IDrive",
name: "IDrive e2",
},
{
key: "IONOS",
name: "IONOS Cloud",
},
{
key: "LyveCloud",
name: "Seagate Lyve Cloud",
},
{
key: "Leviia",
name: "Leviia Object Storage",
},
{
key: "Liara",
name: "Liara Object Storage",
},
{
key: "Linode",
name: "Linode Object Storage",
},
{
key: "Magalu",
name: "Magalu Object Storage",
},
{
key: "Minio",
name: "Minio Object Storage",
},
{
key: "Netease",
name: "Netease Object Storage (NOS)",
},
{
key: "Petabox",
name: "Petabox Object Storage",
},
{
key: "RackCorp",
name: "RackCorp Object Storage",
},
{
key: "Rclone",
name: "Rclone S3 Server",
},
{
key: "Scaleway",
name: "Scaleway Object Storage",
},
{
key: "SeaweedFS",
name: "SeaweedFS S3",
},
{
key: "StackPath",
name: "StackPath Object Storage",
},
{
key: "Storj",
name: "Storj (S3 Compatible Gateway)",
},
{
key: "Synology",
name: "Synology C2 Object Storage",
},
{
key: "TencentCOS",
name: "Tencent Cloud Object Storage (COS)",
},
{
key: "Wasabi",
name: "Wasabi Object Storage",
},
{
key: "Qiniu",
name: "Qiniu Object Storage (Kodo)",
},
{
key: "Other",
name: "Any other S3 compatible provider",
},
];

View File

@@ -35,9 +35,11 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { S3_PROVIDERS } from "./constants";
const updateDestination = z.object({ const updateDestination = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(), accessKeyId: z.string(),
secretAccessKey: z.string(), secretAccessKey: z.string(),
bucket: z.string(), bucket: z.string(),
@@ -70,6 +72,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
api.destination.testConnection.useMutation(); api.destination.testConnection.useMutation();
const form = useForm<UpdateDestination>({ const form = useForm<UpdateDestination>({
defaultValues: { defaultValues: {
provider: "",
accessKeyId: "", accessKeyId: "",
bucket: "", bucket: "",
name: "", name: "",
@@ -152,6 +155,40 @@ export const UpdateDestination = ({ destinationId }: Props) => {
); );
}} }}
/> />
<FormField
control={form.control}
name="provider"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Provider</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a S3 Provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
{S3_PROVIDERS.map((s3Provider) => (
<SelectItem
key={s3Provider.key}
value={s3Provider.key}
>
{s3Provider.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField <FormField
control={form.control} control={form.control}
@@ -285,6 +322,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
variant={"secondary"} variant={"secondary"}
onClick={async () => { onClick={async () => {
await testConnection({ await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"), accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"), bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"), endpoint: form.getValues("endpoint"),
@@ -311,6 +349,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
variant="secondary" variant="secondary"
onClick={async () => { onClick={async () => {
await testConnection({ await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"), accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"), bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"), endpoint: form.getValues("endpoint"),

View File

@@ -397,25 +397,23 @@ export const AddNotification = () => {
)} )}
{type === "discord" && ( {type === "discord" && (
<> <FormField
<FormField control={form.control}
control={form.control} name="webhookUrl"
name="webhookUrl" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>Webhook URL</FormLabel>
<FormLabel>Webhook URL</FormLabel> <FormControl>
<FormControl> <Input
<Input placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ" {...field}
{...field} />
/> </FormControl>
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</>
)} )}
{type === "email" && ( {type === "email" && (

View File

@@ -356,25 +356,23 @@ export const UpdateNotification = ({ notificationId }: Props) => {
)} )}
{type === "discord" && ( {type === "discord" && (
<> <FormField
<FormField control={form.control}
control={form.control} name="webhookUrl"
name="webhookUrl" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>Webhook URL</FormLabel>
<FormLabel>Webhook URL</FormLabel> <FormControl>
<FormControl> <Input
<Input placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ" {...field}
{...field} />
/> </FormControl>
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</>
)} )}
{type === "email" && ( {type === "email" && (
<> <>

View File

@@ -98,7 +98,7 @@ export const ShowServers = () => {
) )
)} )}
{data && data?.length > 0 && ( {data && data?.length > 0 && (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6 overflow-auto">
<Table> <Table>
<TableCaption>See all servers</TableCaption> <TableCaption>See all servers</TableCaption>
<TableHeader> <TableHeader>
@@ -228,21 +228,17 @@ export const ShowServers = () => {
</DropdownMenuItem> </DropdownMenuItem>
</DialogAction> </DialogAction>
{isActive && ( {isActive && server.sshKeyId && (
<> <>
{server.sshKeyId && ( <DropdownMenuSeparator />
<> <DropdownMenuLabel>Extra</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal <ShowTraefikFileSystemModal
serverId={server.serverId} serverId={server.serverId}
/> />
<ShowDockerContainersModal <ShowDockerContainersModal
serverId={server.serverId} serverId={server.serverId}
/> />
</>
)}
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -231,8 +231,8 @@ export const BitbucketIcon = ({ className }: Props) => {
y2="10.814" y2="10.814"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset=".18" stop-color="#0052CC" /> <stop offset=".18" stopColor="#0052CC" />
<stop offset="1" stop-color="#2684FF" /> <stop offset="1" stopColor="#2684FF" />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>

View File

@@ -31,7 +31,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
)} )}
{status === "done" && ( {status === "done" && (
<div <div
className={cn("size-3.5 rounded-full bg-primary", className)} className={cn("size-3.5 rounded-full bg-green-500", className)}
/> />
)} )}
{status === "running" && ( {status === "running" && (

View File

@@ -12,7 +12,7 @@ const buttonVariants = cva(
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", "bg-destructive text-destructive-foreground hover:bg-destructive/70",
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: secondary:

View File

@@ -61,7 +61,7 @@ const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">, React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => ( >(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}> <div ref={ref} {...props}>
<Dot /> <Dot />
</div> </div>
)); ));

View File

@@ -0,0 +1 @@
ALTER TABLE "destination" ADD COLUMN "provider" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "registryUrl" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -316,6 +316,20 @@
"when": 1731875539532, "when": 1731875539532,
"tag": "0044_sour_true_believers", "tag": "0044_sour_true_believers",
"breakpoints": true "breakpoints": true
},
{
"idx": 45,
"version": "6",
"when": 1732644181718,
"tag": "0045_smiling_blur",
"breakpoints": true
},
{
"idx": 46,
"version": "6",
"when": 1732851191048,
"tag": "0046_purple_sleeper",
"breakpoints": true
} }
] ]
} }

View File

@@ -2,7 +2,7 @@
module.exports = { module.exports = {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "pl", "ru", "de", "zh-Hant", "zh-Hans"], locales: ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
localeDetection: false, localeDetection: false,
}, },
fallbackLng: "en", fallbackLng: "en",

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.13.0", "version": "v0.13.1",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -32,10 +32,10 @@ const MyApp = ({
return ( return (
<> <>
<style jsx global>{` <style jsx global>{`
:root { :root {
--font-inter: ${inter.style.fontFamily}; --font-inter: ${inter.style.fontFamily};
} }
`}</style> `}</style>
<Head> <Head>
<title>Dokploy</title> <title>Dokploy</title>
</Head> </Head>
@@ -71,7 +71,17 @@ export default api.withTRPC(
{ {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "pl", "ru", "de", "zh-Hant", "zh-Hans"], locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"zh-Hant",
"zh-Hans",
"fa",
],
localeDetection: false, localeDetection: false,
}, },
fallbackLng: "en", fallbackLng: "en",

View File

@@ -10,137 +10,138 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { extractCommitMessage, extractHash } from "./[refreshToken]"; import { extractCommitMessage, extractHash } from "./[refreshToken]";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse
) { ) {
const signature = req.headers["x-hub-signature-256"]; const signature = req.headers["x-hub-signature-256"];
const githubBody = req.body; const githubBody = req.body;
if (!githubBody?.installation?.id) { if (!githubBody?.installation?.id) {
res.status(400).json({ message: "Github Installation not found" }); res.status(400).json({ message: "Github Installation not found" });
return; return;
} }
const githubResult = await db.query.github.findFirst({ const githubResult = await db.query.github.findFirst({
where: eq(github.githubInstallationId, githubBody.installation.id), where: eq(github.githubInstallationId, githubBody.installation.id),
}); });
if (!githubResult) { if (!githubResult) {
res.status(400).json({ message: "Github Installation not found" }); res.status(400).json({ message: "Github Installation not found" });
return; return;
} }
if (!githubResult.githubWebhookSecret) { if (!githubResult.githubWebhookSecret) {
res.status(400).json({ message: "Github Webhook Secret not set" }); res.status(400).json({ message: "Github Webhook Secret not set" });
return; return;
} }
const webhooks = new Webhooks({ const webhooks = new Webhooks({
secret: githubResult.githubWebhookSecret, secret: githubResult.githubWebhookSecret,
}); });
const verified = await webhooks.verify( const verified = await webhooks.verify(
JSON.stringify(githubBody), JSON.stringify(githubBody),
signature as string, signature as string
); );
if (!verified) { if (!verified) {
res.status(401).json({ message: "Unauthorized" }); res.status(401).json({ message: "Unauthorized" });
return; return;
} }
if (req.headers["x-github-event"] === "ping") { if (req.headers["x-github-event"] === "ping") {
res.status(200).json({ message: "Ping received, webhook is active" }); res.status(200).json({ message: "Ping received, webhook is active" });
return; return;
} }
if (req.headers["x-github-event"] !== "push") { if (req.headers["x-github-event"] !== "push") {
res.status(400).json({ message: "We only accept push events" }); res.status(400).json({ message: "We only accept push events" });
return; return;
} }
try { try {
const branchName = githubBody?.ref?.replace("refs/heads/", ""); const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name; const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body); const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body); const deploymentHash = extractHash(req.headers, req.body);
const apps = await db.query.applications.findMany({ const apps = await db.query.applications.findMany({
where: and( where: and(
eq(applications.sourceType, "github"), eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true), eq(applications.autoDeploy, true),
eq(applications.branch, branchName), eq(applications.branch, branchName),
eq(applications.repository, repository), eq(applications.repository, repository)
), ),
}); });
for (const app of apps) { for (const app of apps) {
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: app.applicationId as string, applicationId: app.applicationId as string,
titleLog: deploymentTitle, titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`, descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy", type: "deploy",
applicationType: "application", applicationType: "application",
server: !!app.serverId, server: !!app.serverId,
}; };
if (IS_CLOUD && app.serverId) { if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId; jobData.serverId = app.serverId;
await deploy(jobData); await deploy(jobData);
return true; return true;
} }
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
{ {
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
}, }
); );
} }
const composeApps = await db.query.compose.findMany({ const composeApps = await db.query.compose.findMany({
where: and( where: and(
eq(compose.sourceType, "github"), eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true), eq(compose.autoDeploy, true),
eq(compose.branch, branchName), eq(compose.branch, branchName),
eq(compose.repository, repository), eq(compose.repository, repository)
), ),
}); });
for (const composeApp of composeApps) { for (const composeApp of composeApps) {
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
composeId: composeApp.composeId as string, composeId: composeApp.composeId as string,
titleLog: deploymentTitle, titleLog: deploymentTitle,
type: "deploy", type: "deploy",
applicationType: "compose", applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`, descriptionLog: `Hash: ${deploymentHash}`,
}; server: !!composeApp.serverId,
};
if (IS_CLOUD && composeApp.serverId) { if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId; jobData.serverId = composeApp.serverId;
await deploy(jobData); await deploy(jobData);
return true; return true;
} }
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
{ {
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
}, }
); );
} }
const totalApps = apps.length + composeApps.length; const totalApps = apps.length + composeApps.length;
const emptyApps = totalApps === 0; const emptyApps = totalApps === 0;
if (emptyApps) { if (emptyApps) {
res.status(200).json({ message: "No apps to deploy" }); res.status(200).json({ message: "No apps to deploy" });
return; return;
} }
res.status(200).json({ message: `Deployed ${totalApps} apps` }); res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) { } catch (error) {
res.status(400).json({ message: "Error To Deploy Application", error }); res.status(400).json({ message: "Error To Deploy Application", error });
} }
} }

View File

@@ -2,6 +2,7 @@ import { AddApplication } from "@/components/dashboard/project/add-application";
import { AddCompose } from "@/components/dashboard/project/add-compose"; import { AddCompose } from "@/components/dashboard/project/add-compose";
import { AddDatabase } from "@/components/dashboard/project/add-database"; import { AddDatabase } from "@/components/dashboard/project/add-database";
import { AddTemplate } from "@/components/dashboard/project/add-template"; import { AddTemplate } from "@/components/dashboard/project/add-template";
import { ProjectEnviroment } from "@/components/dashboard/projects/project-enviroment";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -198,27 +199,35 @@ const Project = (
</div> </div>
{(auth?.rol === "admin" || user?.canCreateServices) && ( {(auth?.rol === "admin" || user?.canCreateServices) && (
<DropdownMenu> <div className="flex flex-row gap-4 flex-wrap">
<DropdownMenuTrigger asChild> <ProjectEnviroment projectId={projectId}>
<Button> <Button variant="outline">Project Enviroment</Button>
<PlusIcon className="h-4 w-4" /> </ProjectEnviroment>
Create Service <DropdownMenu>
</Button> <DropdownMenuTrigger asChild>
</DropdownMenuTrigger> <Button>
<DropdownMenuContent className="w-[200px] space-y-2" align="end"> <PlusIcon className="h-4 w-4" />
<DropdownMenuLabel className="text-sm font-normal "> Create Service
Actions </Button>
</DropdownMenuLabel> </DropdownMenuTrigger>
<DropdownMenuSeparator /> <DropdownMenuContent
<AddApplication className="w-[200px] space-y-2"
projectId={projectId} align="end"
projectName={data?.name} >
/> <DropdownMenuLabel className="text-sm font-normal ">
<AddDatabase projectId={projectId} projectName={data?.name} /> Actions
<AddCompose projectId={projectId} projectName={data?.name} /> </DropdownMenuLabel>
<AddTemplate projectId={projectId} /> <DropdownMenuSeparator />
</DropdownMenuContent> <AddApplication
</DropdownMenu> projectId={projectId}
projectName={data?.name}
/>
<AddDatabase projectId={projectId} projectName={data?.name} />
<AddCompose projectId={projectId} projectName={data?.name} />
<AddTemplate projectId={projectId} />
</DropdownMenuContent>
</DropdownMenu>
</div>
)} )}
</header> </header>
</div> </div>

View File

@@ -1,18 +1,29 @@
import { ShowProjects } from "@/components/dashboard/projects/show"; import { ShowProjects } from "@/components/dashboard/projects/show";
import { ShowWelcomeDokploy } from "@/components/dashboard/settings/billing/show-welcome-dokploy";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import superjson from "superjson"; import superjson from "superjson";
const ShowWelcomeDokploy = dynamic(
() =>
import("@/components/dashboard/settings/billing/show-welcome-dokploy").then(
(mod) => mod.ShowWelcomeDokploy,
),
{ ssr: false },
);
const Dashboard = () => { const Dashboard = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
return ( return (
<> <>
<ShowWelcomeDokploy /> {isCloud && <ShowWelcomeDokploy />}
<ShowProjects /> <ShowProjects />
</> </>
); );

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "ذخیره",
"settings.server.domain.title": "دامنه سرور",
"settings.server.domain.description": "یک دامنه به برنامه سرور خود اضافه کنید.",
"settings.server.domain.form.domain": "دامنه",
"settings.server.domain.form.letsEncryptEmail": "ایمیل Let's Encrypt",
"settings.server.domain.form.certificate.label": "گواهینامه",
"settings.server.domain.form.certificate.placeholder": "یک گواهینامه انتخاب کنید",
"settings.server.domain.form.certificateOptions.none": "هیچکدام",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (پیش‌فرض)",
"settings.server.webServer.title": "وب سرور",
"settings.server.webServer.description": "وب سرور را بازنشانی یا پاک کنید.",
"settings.server.webServer.actions": "اقدامات",
"settings.server.webServer.reload": "بارگذاری مجدد",
"settings.server.webServer.watchLogs": "مشاهده گزارش‌ها",
"settings.server.webServer.updateServerIp": "به‌روزرسانی آی‌پی سرور",
"settings.server.webServer.server.label": "سرور",
"settings.server.webServer.traefik.label": "ترافیک",
"settings.server.webServer.traefik.modifyEnv": "ویرایش محیط",
"settings.server.webServer.storage.label": "فضا",
"settings.server.webServer.storage.cleanUnusedImages": "پاکسازی Image های بدون استفاده",
"settings.server.webServer.storage.cleanUnusedVolumes": "پاک‌سازی ولوم‌های بدون استفاده",
"settings.server.webServer.storage.cleanStoppedContainers": "پاک‌سازی کانتینرهای متوقف‌شده",
"settings.server.webServer.storage.cleanDockerBuilder": "پاک‌سازی بیلدر و سیستم داکر",
"settings.server.webServer.storage.cleanMonitoring": "پاک‌سازی پایش",
"settings.server.webServer.storage.cleanAll": "پاک‌سازی همه",
"settings.profile.title": "حساب کاربری",
"settings.profile.description": "جزئیات پروفایل خود را در اینجا تغییر دهید.",
"settings.profile.email": "ایمیل",
"settings.profile.password": "رمز عبور",
"settings.profile.avatar": "تصویر پروفایل",
"settings.appearance.title": "ظاهر",
"settings.appearance.description": "تم داشبورد خود را سفارشی کنید.",
"settings.appearance.theme": "تم",
"settings.appearance.themeDescription": "یک تم برای داشبورد خود انتخاب کنید",
"settings.appearance.themes.light": "روشن",
"settings.appearance.themes.dark": "تاریک",
"settings.appearance.themes.system": "سیستم",
"settings.appearance.language": "زبان",
"settings.appearance.languageDescription": "یک زبان برای داشبورد خود انتخاب کنید"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "Sauvegarder",
"settings.server.domain.title": "Nom de domaine du serveur",
"settings.server.domain.description": "Ajouter un nom de domaine au serveur de votre application.",
"settings.server.domain.form.domain": "Domaine",
"settings.server.domain.form.letsEncryptEmail": "Adresse email Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificat",
"settings.server.domain.form.certificate.placeholder": "Choisir un certificat",
"settings.server.domain.form.certificateOptions.none": "Aucun",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Par défaut)",
"settings.server.webServer.title": "Serveur web",
"settings.server.webServer.description": "Recharger ou nettoyer le serveur web.",
"settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Recharger",
"settings.server.webServer.watchLogs": "Consulter les logs",
"settings.server.webServer.updateServerIp": "Mettre à jour l'IP du serveur",
"settings.server.webServer.server.label": "Serveur",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modifier les variables d'environnement",
"settings.server.webServer.storage.label": "Stockage",
"settings.server.webServer.storage.cleanUnusedImages": "Supprimer les images inutilisées",
"settings.server.webServer.storage.cleanUnusedVolumes": "Supprimer les volumes inutilisés",
"settings.server.webServer.storage.cleanStoppedContainers": "Supprimer les conteneurs arrêtés",
"settings.server.webServer.storage.cleanDockerBuilder": "Nettoyer le Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Nettoyer le monitoring",
"settings.server.webServer.storage.cleanAll": "Tout nettoyer",
"settings.profile.title": "Compte",
"settings.profile.description": "Modifier les informations de votre compte ici.",
"settings.profile.email": "Adresse Email",
"settings.profile.password": "Mot de passe",
"settings.profile.avatar": "Photo de profil",
"settings.appearance.title": "Apparence",
"settings.appearance.description": "Customiser le thème de votre dashboard.",
"settings.appearance.theme": "Thème",
"settings.appearance.themeDescription": "Choisir un thème pour votre dashboard",
"settings.appearance.themes.light": "Clair",
"settings.appearance.themes.dark": "Sombre",
"settings.appearance.themes.system": "Système",
"settings.appearance.language": "Langue",
"settings.appearance.languageDescription": "Sélectionner une langue pour votre dashboard"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "Kaydet",
"settings.server.domain.title": "Sunucu Alanı",
"settings.server.domain.description": "Sunucu uygulamanıza bir alan adı ekleyin.",
"settings.server.domain.form.domain": "Alan Adı",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-postası",
"settings.server.domain.form.certificate.label": "Sertifika",
"settings.server.domain.form.certificate.placeholder": "Bir sertifika seçin",
"settings.server.domain.form.certificateOptions.none": "Hiçbiri",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Varsayılan)",
"settings.server.webServer.title": "Web Sunucusu",
"settings.server.webServer.description": "Web sunucusunu yeniden yükleyin veya temizleyin.",
"settings.server.webServer.actions": "İşlemler",
"settings.server.webServer.reload": "Yeniden Yükle",
"settings.server.webServer.watchLogs": "Günlükleri İzle",
"settings.server.webServer.updateServerIp": "Sunucu IP'sini Güncelle",
"settings.server.webServer.server.label": "Sunucu",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Değiştir",
"settings.server.webServer.storage.label": "Alan",
"settings.server.webServer.storage.cleanUnusedImages": "Kullanılmayan görüntüleri temizle",
"settings.server.webServer.storage.cleanUnusedVolumes": "Kullanılmayan birimleri temizle",
"settings.server.webServer.storage.cleanStoppedContainers": "Durmuş konteynerleri temizle",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder ve Sistemi Temizle",
"settings.server.webServer.storage.cleanMonitoring": "İzlemeyi Temizle",
"settings.server.webServer.storage.cleanAll": "Hepsini temizle",
"settings.profile.title": "Hesap",
"settings.profile.description": "Profil detaylarınızı buradan değiştirebilirsiniz.",
"settings.profile.email": "E-posta",
"settings.profile.password": "Şifre",
"settings.profile.avatar": "Profil Resmi",
"settings.appearance.title": "Görünüm",
"settings.appearance.description": "Kontrol panelinin temasını özelleştirin.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Kontrol paneli için bir tema seçin",
"settings.appearance.themes.light": "Açık",
"settings.appearance.themes.dark": "Koyu",
"settings.appearance.themes.system": "Sistem",
"settings.appearance.language": "Dil",
"settings.appearance.languageDescription": "Kontrol paneli için bir dil seçin"
}

View File

@@ -384,6 +384,7 @@ export const applicationRouter = createTRPCRouter({
password: input.password, password: input.password,
sourceType: "docker", sourceType: "docker",
applicationStatus: "idle", applicationStatus: "idle",
registryUrl: input.registryUrl,
}); });
return true; return true;

View File

@@ -40,11 +40,10 @@ export const destinationRouter = createTRPCRouter({
testConnection: adminProcedure testConnection: adminProcedure
.input(apiCreateDestination) .input(apiCreateDestination)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey } = input; const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
input;
try { try {
const rcloneFlags = [ const rcloneFlags = [
// `--s3-provider=Cloudflare`,
`--s3-access-key-id=${accessKey}`, `--s3-access-key-id=${accessKey}`,
`--s3-secret-access-key=${secretAccessKey}`, `--s3-secret-access-key=${secretAccessKey}`,
`--s3-region=${region}`, `--s3-region=${region}`,
@@ -52,6 +51,9 @@ export const destinationRouter = createTRPCRouter({
"--s3-no-check-bucket", "--s3-no-check-bucket",
"--s3-force-path-style", "--s3-force-path-style",
]; ];
if (provider) {
rcloneFlags.unshift(`--s3-provider=${provider}`);
}
const rcloneDestination = `:s3:${bucket}`; const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;

View File

@@ -3,173 +3,173 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%; --card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%; --primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%; --secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%; --muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%; --accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%; --border: 240 5.9% 90%;
--input: 240 5.9% 90%; --input: 240 5.9% 90%;
--ring: 240 10% 3.9%; --ring: 240 10% 3.9%;
--radius: 0.5rem; --radius: 0.5rem;
--overlay: rgba(0, 0, 0, 0.2); --overlay: rgba(0, 0, 0, 0.2);
--chart-1: 173 58% 39%; --chart-1: 173 58% 39%;
--chart-2: 12 76% 61%; --chart-2: 12 76% 61%;
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
} }
.dark { .dark {
--background: 0 0% 0%; --background: 0 0% 0%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 240 4% 10%; --card: 240 4% 10%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%; --popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--primary: 0 0% 98%; --primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%; --secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--muted: 240 4% 10%; --muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%; --accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 4% 10%; --input: 240 4% 10%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
--overlay: rgba(0, 0, 0, 0.5); --overlay: rgba(0, 0, 0, 0.5);
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-5: 160 60% 45%; --chart-5: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-2: 340 75% 55%; --chart-2: 340 75% 55%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
.xterm-viewport { .xterm-viewport {
border-radius: 0.75rem /* 12px */ !important; border-radius: 0.75rem /* 12px */ !important;
} }
.xterm .xterm-viewport { .xterm .xterm-viewport {
overflow-y: auto !important; overflow-y: auto !important;
} }
.xterm .xterm-screen { .xterm .xterm-screen {
overflow: hidden; overflow: hidden;
} }
@layer utilities { @layer utilities {
/* Chrome, Safari and Opera */ /* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
.no-scrollbar { .no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
} }
/* Codemirror */ /* Codemirror */
.cm-editor { .cm-editor {
@apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none;
} }
.cm-editor .cm-scroller { .cm-editor .cm-scroller {
font-family: inherit; font-family: inherit;
line-height: inherit; line-height: inherit;
} }
.cm-editor.cm-focused { .cm-editor.cm-focused {
@apply outline-none; @apply outline-none;
} }
/* fix: placeholder bg */ /* fix: placeholder bg */
.cm-editor .cm-activeLine:has(.cm-placeholder) { .cm-editor .cm-activeLine:has(.cm-placeholder) {
background-color: transparent; background-color: transparent;
} }
.compose-file-editor .cm-editor { .compose-file-editor .cm-editor {
@apply min-h-[25rem]; @apply min-h-[25rem];
} }
@keyframes heartbeat { @keyframes heartbeat {
0% { 0% {
transform: scale(1); transform: scale(1);
opacity: 0.7; opacity: 0.7;
} }
25% { 25% {
transform: scale(1.1); transform: scale(1.1);
opacity: 1; opacity: 1;
} }
50% { 50% {
transform: scale(1); transform: scale(1);
opacity: 0.7; opacity: 0.7;
} }
75% { 75% {
transform: scale(1.1); transform: scale(1.1);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: scale(1); transform: scale(1);
opacity: 0.7; opacity: 0.7;
} }
} }
.animate-heartbeat { .animate-heartbeat {
animation: heartbeat 2.5s infinite; animation: heartbeat 2.5s infinite;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.swagger-ui { .swagger-ui {
background-color: white; background-color: white;
} }
.swagger-ui .info { .swagger-ui .info {
margin: 0px !important; margin: 0px !important;
padding-top: 1rem !important; padding-top: 1rem !important;
} }
} }

View File

@@ -4,9 +4,12 @@ const SUPPORTED_LOCALES = [
"en", "en",
"pl", "pl",
"ru", "ru",
"fr",
"de", "de",
"tr",
"zh-Hant", "zh-Hant",
"zh-Hans", "zh-Hans",
"fa",
] as const; ] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number]; type Locale = (typeof SUPPORTED_LOCALES)[number];

45
lefthook.yml Normal file
View File

@@ -0,0 +1,45 @@
# EXAMPLE USAGE:
#
# Refer for explanation to following link:
# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
#
# pre-push:
# commands:
# packages-audit:
# tags: frontend security
# run: yarn audit
# gems-audit:
# tags: backend security
# run: bundle audit
#
# pre-commit:
# parallel: true
# commands:
# eslint:
# glob: "*.{js,ts,jsx,tsx}"
# run: yarn eslint {staged_files}
# rubocop:
# tags: backend style
# glob: "*.rb"
# exclude: '(^|/)(application|routes)\.rb$'
# run: bundle exec rubocop --force-exclusion {all_files}
# govet:
# tags: backend style
# files: git ls-files -m
# glob: "*.go"
# run: go vet {files}
# scripts:
# "hello.js":
# runner: node
# "any.go":
# runner: go run
commit-msg:
commands:
commitlint:
run: "npx commitlint --edit $1"
pre-commit:
commands:
check:
run: "pnpm check"

View File

@@ -13,20 +13,18 @@
"server:build": "pnpm --filter=server run build", "server:build": "pnpm --filter=server run build",
"docker:build:canary": "./apps/dokploy/docker/build.sh canary", "docker:build:canary": "./apps/dokploy/docker/build.sh canary",
"typecheck": "pnpm -r run typecheck", "typecheck": "pnpm -r run typecheck",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"build": "pnpm -r run build", "build": "pnpm -r run build",
"format-and-lint": "biome check .", "format-and-lint": "biome check .",
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", "check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true",
"format-and-lint:fix": "biome check . --write", "format-and-lint:fix": "biome check . --write"
"prepare": "node .husky/install.mjs"
}, },
"devDependencies": { "devDependencies": {
"dotenv": "16.4.5", "dotenv": "16.4.5",
"esbuild": "0.20.2", "esbuild": "0.20.2",
"tsx": "4.16.2", "tsx": "4.16.2",
"lint-staged": "^15.2.7", "lint-staged": "^15.2.7",
"@biomejs/biome": "1.8.3", "@biomejs/biome": "1.9.4",
"husky": "^9.1.6", "lefthook": "1.8.4",
"@commitlint/cli": "^19.3.0", "@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2", "@commitlint/config-conventional": "^19.2.2",
"@types/node": "^18.17.0" "@types/node": "^18.17.0"

View File

@@ -147,6 +147,7 @@ export const applications = pgTable("application", {
username: text("username"), username: text("username"),
password: text("password"), password: text("password"),
dockerImage: text("dockerImage"), dockerImage: text("dockerImage"),
registryUrl: text("registryUrl"),
// Git // Git
customGitUrl: text("customGitUrl"), customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"), customGitBranch: text("customGitBranch"),
@@ -348,6 +349,7 @@ const createSchema = createInsertSchema(applications, {
dockerImage: z.string().optional(), dockerImage: z.string().optional(),
username: z.string().optional(), username: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
registryUrl: z.string().optional(),
customGitSSHKeyId: z.string().optional(), customGitSSHKeyId: z.string().optional(),
repository: z.string().optional(), repository: z.string().optional(),
dockerfile: z.string().optional(), dockerfile: z.string().optional(),
@@ -451,6 +453,7 @@ export const apiSaveDockerProvider = createSchema
applicationId: true, applicationId: true,
username: true, username: true,
password: true, password: true,
registryUrl: true,
}) })
.required(); .required();

View File

@@ -12,6 +12,7 @@ export const destinations = pgTable("destination", {
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
name: text("name").notNull(), name: text("name").notNull(),
provider: text("provider"),
accessKey: text("accessKey").notNull(), accessKey: text("accessKey").notNull(),
secretAccessKey: text("secretAccessKey").notNull(), secretAccessKey: text("secretAccessKey").notNull(),
bucket: text("bucket").notNull(), bucket: text("bucket").notNull(),
@@ -37,6 +38,7 @@ export const destinationsRelations = relations(
const createSchema = createInsertSchema(destinations, { const createSchema = createInsertSchema(destinations, {
destinationId: z.string(), destinationId: z.string(),
name: z.string().min(1), name: z.string().min(1),
provider: z.string(),
accessKey: z.string(), accessKey: z.string(),
bucket: z.string(), bucket: z.string(),
endpoint: z.string(), endpoint: z.string(),
@@ -47,6 +49,7 @@ const createSchema = createInsertSchema(destinations, {
export const apiCreateDestination = createSchema export const apiCreateDestination = createSchema
.pick({ .pick({
name: true, name: true,
provider: true,
accessKey: true, accessKey: true,
bucket: true, bucket: true,
region: true, region: true,

View File

@@ -52,7 +52,7 @@ export const BuildFailedEmail = ({
<Section className="mt-[32px]"> <Section className="mt-[32px]">
<Img <Img
src={ src={
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png" "https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
} }
width="100" width="100"
height="50" height="50"

View File

@@ -50,7 +50,7 @@ export const BuildSuccessEmail = ({
<Section className="mt-[32px]"> <Section className="mt-[32px]">
<Img <Img
src={ src={
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png" "https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
} }
width="100" width="100"
height="50" height="50"

View File

@@ -51,7 +51,7 @@ export const DatabaseBackupEmail = ({
<Section className="mt-[32px]"> <Section className="mt-[32px]">
<Img <Img
src={ src={
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png" "https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
} }
width="100" width="100"
height="50" height="50"

View File

@@ -44,7 +44,7 @@ export const DockerCleanupEmail = ({
<Section className="mt-[32px]"> <Section className="mt-[32px]">
<Img <Img
src={ src={
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png" "https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
} }
width="100" width="100"
height="50" height="50"

View File

@@ -41,7 +41,7 @@ export const DokployRestartEmail = ({
<Section className="mt-[32px]"> <Section className="mt-[32px]">
<Img <Img
src={ src={
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png" "https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
} }
width="100" width="100"
height="50" height="50"

View File

@@ -49,7 +49,7 @@ export const InvitationEmail = ({
<Section className="mt-[32px]"> <Section className="mt-[32px]">
<Img <Img
src={ src={
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png" "https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
} }
width="100" width="100"
height="50" height="50"

View File

@@ -208,14 +208,6 @@ export const deployApplication = async ({
adminId: application.project.adminId, adminId: application.project.adminId,
}); });
console.log(
"Error on ",
application.buildType,
"/",
application.sourceType,
error,
);
throw error; throw error;
} }

View File

@@ -28,9 +28,9 @@ export const removeScheduleBackup = (backupId: string) => {
}; };
export const getS3Credentials = (destination: Destination) => { export const getS3Credentials = (destination: Destination) => {
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination; const { accessKey, secretAccessKey, bucket, region, endpoint, provider } =
destination;
const rcloneFlags = [ const rcloneFlags = [
// `--s3-provider=Cloudflare`,
`--s3-access-key-id=${accessKey}`, `--s3-access-key-id=${accessKey}`,
`--s3-secret-access-key=${secretAccessKey}`, `--s3-secret-access-key=${secretAccessKey}`,
`--s3-region=${region}`, `--s3-region=${region}`,
@@ -39,5 +39,9 @@ export const getS3Credentials = (destination: Destination) => {
"--s3-force-path-style", "--s3-force-path-style",
]; ];
if (provider) {
rcloneFlags.unshift(`--s3-provider=${provider}`);
}
return rcloneFlags; return rcloneFlags;
}; };

View File

@@ -11,12 +11,13 @@ import type { MysqlNested } from "../databases/mysql";
import type { PostgresNested } from "../databases/postgres"; import type { PostgresNested } from "../databases/postgres";
import type { RedisNested } from "../databases/redis"; import type { RedisNested } from "../databases/redis";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import { getRemoteDocker } from "../servers/remote-docker"; import { getRemoteDocker } from "../servers/remote-docker";
interface RegistryAuth { interface RegistryAuth {
username: string; username: string;
password: string; password: string;
serveraddress: string; registryUrl: string;
} }
export const pullImage = async ( export const pullImage = async (
@@ -29,29 +30,21 @@ export const pullImage = async (
throw new Error("Docker image not found"); throw new Error("Docker image not found");
} }
await new Promise((resolve, reject) => { if (authConfig?.username && authConfig?.password) {
docker.pull(dockerImage, { authconfig: authConfig }, (err, stream) => { await spawnAsync(
if (err) { "docker",
reject(err); [
return; "login",
} authConfig.registryUrl || "",
"-u",
docker.modem.followProgress( authConfig.username,
stream as Readable, "-p",
(err: Error | null, res) => { authConfig.password,
if (!err) { ],
resolve(res); onData,
} );
if (err) { }
reject(err); await spawnAsync("docker", ["pull", dockerImage], onData);
}
},
(event) => {
onData?.(event);
},
);
});
});
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -5,7 +5,7 @@ import { pullImage } from "../docker/utils";
interface RegistryAuth { interface RegistryAuth {
username: string; username: string;
password: string; password: string;
serveraddress: string; registryUrl: string;
} }
export const buildDocker = async ( export const buildDocker = async (
@@ -16,6 +16,7 @@ export const buildDocker = async (
const authConfig: Partial<RegistryAuth> = { const authConfig: Partial<RegistryAuth> = {
username: username || "", username: username || "",
password: password || "", password: password || "",
registryUrl: application.registryUrl || "",
}; };
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
@@ -33,7 +34,7 @@ export const buildDocker = async (
dockerImage, dockerImage,
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(`${data.status}\n`); writeStream.write(`${data}\n`);
} }
}, },
authConfig, authConfig,
@@ -41,7 +42,7 @@ export const buildDocker = async (
await mechanizeDockerContainer(application); await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n"); writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) { } catch (error) {
writeStream.write(`ERROR: ${error}: ❌`); writeStream.write("❌ Error");
throw error; throw error;
} finally { } finally {
writeStream.end(); writeStream.end();

View File

@@ -206,7 +206,7 @@ export const getGithubCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};

184
pnpm-lock.yaml generated
View File

@@ -13,8 +13,8 @@ importers:
.: .:
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: 1.8.3 specifier: 1.9.4
version: 1.8.3 version: 1.9.4
'@commitlint/cli': '@commitlint/cli':
specifier: ^19.3.0 specifier: ^19.3.0
version: 19.3.0(@types/node@18.19.42)(typescript@5.5.3) version: 19.3.0(@types/node@18.19.42)(typescript@5.5.3)
@@ -30,9 +30,9 @@ importers:
esbuild: esbuild:
specifier: 0.20.2 specifier: 0.20.2
version: 0.20.2 version: 0.20.2
husky: lefthook:
specifier: ^9.1.6 specifier: 1.8.4
version: 9.1.6 version: 1.8.4
lint-staged: lint-staged:
specifier: ^15.2.7 specifier: ^15.2.7
version: 15.2.7 version: 15.2.7
@@ -682,55 +682,55 @@ packages:
'@balena/dockerignore@1.0.2': '@balena/dockerignore@1.0.2':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
'@biomejs/biome@1.8.3': '@biomejs/biome@1.9.4':
resolution: {integrity: sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==} resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
'@biomejs/cli-darwin-arm64@1.8.3': '@biomejs/cli-darwin-arm64@1.9.4':
resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==} resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@1.8.3': '@biomejs/cli-darwin-x64@1.9.4':
resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==} resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.8.3': '@biomejs/cli-linux-arm64-musl@1.9.4':
resolution: {integrity: sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==} resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-arm64@1.8.3': '@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==} resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64-musl@1.8.3': '@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==} resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64@1.8.3': '@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==} resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-win32-arm64@1.8.3': '@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==} resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@biomejs/cli-win32-x64@1.8.3': '@biomejs/cli-win32-x64@1.9.4':
resolution: {integrity: sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==} resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -4722,11 +4722,6 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'} engines: {node: '>=16.17.0'}
husky@9.1.6:
resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==}
engines: {node: '>=18'}
hasBin: true
hyperdyperid@1.2.0: hyperdyperid@1.2.0:
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
engines: {node: '>=10.18'} engines: {node: '>=10.18'}
@@ -4958,6 +4953,60 @@ packages:
leac@0.6.0: leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
lefthook-darwin-arm64@1.8.4:
resolution: {integrity: sha512-OS5MsU0gvd8LYSpuQCHtmDUqwNrJ/LjCO0LGC1wNepY4OkuVl9DfX+rQ506CVUQYZiGVcwy2/qPOOBjNzA5+wQ==}
cpu: [arm64]
os: [darwin]
lefthook-darwin-x64@1.8.4:
resolution: {integrity: sha512-QLRsqK9aTMRcVW8qz4pzI2OWnGCEcaEPJlIiFjwstYsS+wfkooxOS0UkfVMjy+QoGgEcki+cxF/FoY7lE7DDtw==}
cpu: [x64]
os: [darwin]
lefthook-freebsd-arm64@1.8.4:
resolution: {integrity: sha512-chnQ1m/Cmn9c0sLdk5HL2SToE5LBJv5uQMdH1IGRRcw+nEqWqrMnDXvM75caiJAyjmUGvPH3czKTJDzTFV1E+A==}
cpu: [arm64]
os: [freebsd]
lefthook-freebsd-x64@1.8.4:
resolution: {integrity: sha512-KQi+WBUdnGLnK0rHOR58kbMH5TDVN1ZjZLu66Pv9FCG7Y7shR1qtaTXu+wmxdRhMvaLeQIXRsUEPjNRC66yMmA==}
cpu: [x64]
os: [freebsd]
lefthook-linux-arm64@1.8.4:
resolution: {integrity: sha512-CXNcqIskLwTwQARidGdFqmNxpvOU3jsWPK4KA7pq2+QmlWJ64w98ebMvNBoUmRUCXqzmUm7Udf/jpfz2fobewQ==}
cpu: [arm64]
os: [linux]
lefthook-linux-x64@1.8.4:
resolution: {integrity: sha512-pVNITkFBxUCEtamWSM/res2Gd48+m9YKbNyIBndAuZVC5pKV5aGKZy2DNq6PWUPYiUDPx+7hoAtCJg/tlAiqhw==}
cpu: [x64]
os: [linux]
lefthook-openbsd-arm64@1.8.4:
resolution: {integrity: sha512-l+i/Dg5X36kYzhpMGSPE3rMbWy1KSytbLB9lY1PmxYb6LRH6iQTYIoxvLabVUwSBPSq8HtIFa50+bvC5+scfVA==}
cpu: [arm64]
os: [openbsd]
lefthook-openbsd-x64@1.8.4:
resolution: {integrity: sha512-CqhDDPPX8oHzMLgNi/Reba823DRzj+eMNWQ8axvSiIG+zmG1w20xZH5QSs/mD3tjrND90yfDd90mWMt181qPyA==}
cpu: [x64]
os: [openbsd]
lefthook-windows-arm64@1.8.4:
resolution: {integrity: sha512-dvpvorICmVjmw29Aiczg7DcaSzkd86bEBomiGq4UsAEk3+7ExLrlWJDLFsI6xLjMKmTxy+F7eXb2uDtuFC1N4g==}
cpu: [arm64]
os: [win32]
lefthook-windows-x64@1.8.4:
resolution: {integrity: sha512-e+y8Jt4/7PnoplhOuK48twjGVJEsU4T3J5kxD4mWfl6Cbit0YSn4bme9nW41eqCqTUqOm+ky29XlfnPHFX5ZNA==}
cpu: [x64]
os: [win32]
lefthook@1.8.4:
resolution: {integrity: sha512-XNyMaTWNRuADOaocYiHidgNkNDz8SCekpdNJ7lqceFcBT2zjumnb28/o7IMaNROpLBZdQkLkJXSeaQWGqn3kog==}
hasBin: true
lilconfig@2.1.0: lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -6820,39 +6869,39 @@ snapshots:
'@balena/dockerignore@1.0.2': {} '@balena/dockerignore@1.0.2': {}
'@biomejs/biome@1.8.3': '@biomejs/biome@1.9.4':
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.8.3 '@biomejs/cli-darwin-arm64': 1.9.4
'@biomejs/cli-darwin-x64': 1.8.3 '@biomejs/cli-darwin-x64': 1.9.4
'@biomejs/cli-linux-arm64': 1.8.3 '@biomejs/cli-linux-arm64': 1.9.4
'@biomejs/cli-linux-arm64-musl': 1.8.3 '@biomejs/cli-linux-arm64-musl': 1.9.4
'@biomejs/cli-linux-x64': 1.8.3 '@biomejs/cli-linux-x64': 1.9.4
'@biomejs/cli-linux-x64-musl': 1.8.3 '@biomejs/cli-linux-x64-musl': 1.9.4
'@biomejs/cli-win32-arm64': 1.8.3 '@biomejs/cli-win32-arm64': 1.9.4
'@biomejs/cli-win32-x64': 1.8.3 '@biomejs/cli-win32-x64': 1.9.4
'@biomejs/cli-darwin-arm64@1.8.3': '@biomejs/cli-darwin-arm64@1.9.4':
optional: true optional: true
'@biomejs/cli-darwin-x64@1.8.3': '@biomejs/cli-darwin-x64@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-arm64-musl@1.8.3': '@biomejs/cli-linux-arm64-musl@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-arm64@1.8.3': '@biomejs/cli-linux-arm64@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-x64-musl@1.8.3': '@biomejs/cli-linux-x64-musl@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-x64@1.8.3': '@biomejs/cli-linux-x64@1.9.4':
optional: true optional: true
'@biomejs/cli-win32-arm64@1.8.3': '@biomejs/cli-win32-arm64@1.9.4':
optional: true optional: true
'@biomejs/cli-win32-x64@1.8.3': '@biomejs/cli-win32-x64@1.9.4':
optional: true optional: true
'@braintree/sanitize-url@7.0.2': {} '@braintree/sanitize-url@7.0.2': {}
@@ -10940,8 +10989,6 @@ snapshots:
human-signals@5.0.0: {} human-signals@5.0.0: {}
husky@9.1.6: {}
hyperdyperid@1.2.0: {} hyperdyperid@1.2.0: {}
i18next-fs-backend@2.3.2: {} i18next-fs-backend@2.3.2: {}
@@ -11153,6 +11200,49 @@ snapshots:
leac@0.6.0: {} leac@0.6.0: {}
lefthook-darwin-arm64@1.8.4:
optional: true
lefthook-darwin-x64@1.8.4:
optional: true
lefthook-freebsd-arm64@1.8.4:
optional: true
lefthook-freebsd-x64@1.8.4:
optional: true
lefthook-linux-arm64@1.8.4:
optional: true
lefthook-linux-x64@1.8.4:
optional: true
lefthook-openbsd-arm64@1.8.4:
optional: true
lefthook-openbsd-x64@1.8.4:
optional: true
lefthook-windows-arm64@1.8.4:
optional: true
lefthook-windows-x64@1.8.4:
optional: true
lefthook@1.8.4:
optionalDependencies:
lefthook-darwin-arm64: 1.8.4
lefthook-darwin-x64: 1.8.4
lefthook-freebsd-arm64: 1.8.4
lefthook-freebsd-x64: 1.8.4
lefthook-linux-arm64: 1.8.4
lefthook-linux-x64: 1.8.4
lefthook-openbsd-arm64: 1.8.4
lefthook-openbsd-x64: 1.8.4
lefthook-windows-arm64: 1.8.4
lefthook-windows-x64: 1.8.4
lilconfig@2.1.0: {} lilconfig@2.1.0: {}
lilconfig@3.1.2: {} lilconfig@3.1.2: {}