Merge branch 'canary' into i18n-french

This commit is contained in:
Mauricio Siu
2024-11-28 20:39:25 -06:00
25 changed files with 5099 additions and 298 deletions

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

@@ -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

@@ -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

@@ -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

@@ -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,7 +37,7 @@ 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", "fr", "de", "zh-Hant", "zh-Hans"], { language: z.enum(["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans"], {
required_error: "Please select a language.", required_error: "Please select a language.",
}), }),
}); });
@@ -180,6 +180,7 @@ export function AppearanceForm() {
{ 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" },
].map((preset) => ( ].map((preset) => (
<SelectItem key={preset.label} value={preset.value}> <SelectItem key={preset.label} value={preset.value}>
{preset.label} {preset.label}

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

@@ -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

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

File diff suppressed because it is too large Load Diff

View File

@@ -316,6 +316,13 @@
"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
} }
] ]
} }

View File

@@ -1,10 +1,10 @@
/** @type {import('next-i18next').UserConfig} */ /** @type {import('next-i18next').UserConfig} */
module.exports = { module.exports = {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "pl", "ru", "fr", "de", "zh-Hant", "zh-Hans"], locales: ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans"],
localeDetection: false, localeDetection: false,
}, },
fallbackLng: "en", fallbackLng: "en",
keySeparator: false, keySeparator: false,
}; };

View File

@@ -71,7 +71,7 @@ export default api.withTRPC(
{ {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "pl", "ru", "fr", "de", "zh-Hant", "zh-Hans"], locales: ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans"],
localeDetection: false, localeDetection: false,
}, },
fallbackLng: "en", fallbackLng: "en",

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

@@ -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

@@ -64,7 +64,7 @@
--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%;

View File

@@ -6,6 +6,7 @@ const SUPPORTED_LOCALES = [
"ru", "ru",
"fr", "fr",
"de", "de",
"tr",
"zh-Hant", "zh-Hant",
"zh-Hans", "zh-Hans",
] as const; ] as const;

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

@@ -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;
}; };