Merge branch 'canary' into i18n-french

This commit is contained in:
Mauricio Siu 2024-11-28 20:39:25 -06:00
commit 5853117e5f
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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
applicationId: string;
}
export const DeleteApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.application.delete.useMutation();
const { data } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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 delete succesfully");
toast.success("Application deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to delete Application");
toast.error("Error deleting the application");
});
} else {
form.setError("projectName", {
message: "Project name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
application. If you are sure please enter the application name to
delete this application.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-application"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
composeId: string;
}
export const DeleteCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
const { data } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
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 (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
compose and all its services.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Compose delete succesfully");
})
.catch(() => {
toast.error("Error to delete the compose");
});
compose. If you are sure please enter the compose name to delete
this compose.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-compose"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
mariadbId: string;
}
export const DeleteMariadb = ({ mariadbId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
const { data } = api.mariadb.one.useQuery(
{ mariadbId },
{ enabled: !!mariadbId },
);
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mariadbId,
})
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 delete succesfully");
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to delete the database");
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mariadb"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
mongoId: string;
}
// commen
export const DeleteMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId });
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mongoId,
})
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 delete succesfully");
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to delete the database");
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mongo"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
mysqlId: string;
}
export const DeleteMysql = ({ mysqlId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId });
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mysqlId,
})
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 delete succesfully");
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to delete the database");
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mysql"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
postgresId: string;
}
export const DeletePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
const { data } = api.postgres.one.useQuery(
{ postgresId },
{ enabled: !!postgresId },
);
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
postgresId,
})
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 delete succesfully");
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to delete the database");
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-postgres"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</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 {
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 { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
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 {
redisId: string;
}
export const DeleteRedis = ({ redisId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
const { data } = api.redis.one.useQuery({ redisId }, { enabled: !!redisId });
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
redisId,
})
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 delete succesfully");
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to delete the database");
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-redis"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -37,7 +37,7 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], {
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.",
}),
});
@ -180,6 +180,7 @@ export function AppearanceForm() {
{ label: "Deutsch", value: "de" },
{ label: "繁體中文", value: "zh-Hant" },
{ label: "简体中文", value: "zh-Hans" },
{ label: "Türkçe", value: "tr" },
].map((preset) => (
<SelectItem key={preset.label} value={preset.value}>
{preset.label}

View File

@ -34,9 +34,11 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({
name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
@ -58,6 +60,7 @@ export const AddDestination = () => {
api.destination.testConnection.useMutation();
const form = useForm<AddDestination>({
defaultValues: {
provider: "",
accessKeyId: "",
bucket: "",
name: "",
@ -73,6 +76,7 @@ export const AddDestination = () => {
const onSubmit = async (data: AddDestination) => {
await mutateAsync({
provider: data.provider,
accessKey: data.accessKeyId,
bucket: data.bucket,
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
control={form.control}
@ -255,6 +293,7 @@ export const AddDestination = () => {
isLoading={isLoading}
onClick={async () => {
await testConnection({
provider: form.getValues("provider"),
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
@ -283,6 +322,7 @@ export const AddDestination = () => {
variant="secondary"
onClick={async () => {
await testConnection({
provider: form.getValues("provider"),
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
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 { toast } from "sonner";
import { z } from "zod";
import { S3_PROVIDERS } from "./constants";
const updateDestination = z.object({
name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
@ -70,6 +72,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
api.destination.testConnection.useMutation();
const form = useForm<UpdateDestination>({
defaultValues: {
provider: "",
accessKeyId: "",
bucket: "",
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
control={form.control}
@ -285,6 +322,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
variant={"secondary"}
onClick={async () => {
await testConnection({
provider: form.getValues("provider"),
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
@ -311,6 +349,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
variant="secondary"
onClick={async () => {
await testConnection({
provider: form.getValues("provider"),
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),

View File

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

View File

@ -12,7 +12,7 @@ const buttonVariants = cva(
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/70",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
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,
"tag": "0044_sour_true_believers",
"breakpoints": true
},
{
"idx": 45,
"version": "6",
"when": 1732644181718,
"tag": "0045_smiling_blur",
"breakpoints": true
}
]
}

View File

@ -2,7 +2,7 @@
module.exports = {
i18n: {
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,
},
fallbackLng: "en",

View File

@ -71,7 +71,7 @@ export default api.withTRPC(
{
i18n: {
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,
},
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
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
input;
try {
const rcloneFlags = [
// `--s3-provider=Cloudflare`,
`--s3-access-key-id=${accessKey}`,
`--s3-secret-access-key=${secretAccessKey}`,
`--s3-region=${region}`,
@ -52,6 +51,9 @@ export const destinationRouter = createTRPCRouter({
"--s3-no-check-bucket",
"--s3-force-path-style",
];
if (provider) {
rcloneFlags.unshift(`--s3-provider=${provider}`);
}
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;

View File

@ -64,7 +64,7 @@
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;

View File

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

View File

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

View File

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