diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index 770d1542..f7dfe10b 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -39,7 +39,8 @@ export default async function RoutePage(props: PageParams<{ const dbItem = await db.query.database.findFirst({ where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)), with: { - project: true + project: true, + retentionPolicy: true } }); diff --git a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx new file mode 100644 index 00000000..99f52676 --- /dev/null +++ b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx @@ -0,0 +1,306 @@ +"use client" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + useZodForm +} from "@/components/ui/form"; +import {DatabaseType} from "@/components/wrappers/dashboard/database/database-form/form-database.schema"; +import {RetentionSettings, RetentionSettingsSchema} from "./backup-retention-settings.schema"; +import {useMutation} from "@tanstack/react-query"; +import {updateOrCreateBackupRetentionPolicyAction} from "./backup-retention-settings.action"; +import {Database, DatabaseWith, RetentionPolicy} from "@/db/schema/07_database"; +import {toast} from "sonner"; +import {useRouter} from "next/navigation"; +import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group"; +import {Badge} from "@/components/ui/badge"; +import {Separator} from "@/components/ui/separator"; +import {Input} from "@/components/ui/input"; +import {Button} from "@/components/ui/button"; +import {Calendar, Save} from "lucide-react"; + + +export type BackupRetentionSettingsFormProps = { + defaultValues?: RetentionPolicy; + database: DatabaseWith; +}; + + +export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRetentionSettingsFormProps) => { + const router = useRouter(); + + const form = useZodForm({ + schema: RetentionSettingsSchema, + defaultValues: { + type: defaultValues?.type ?? "gfs", + count: defaultValues?.count ?? 7, + days: defaultValues?.days ?? 30, + gfs: { + daily: defaultValues?.gfsDaily ?? 7, + weekly: defaultValues?.gfsWeekly ?? 4, + monthly: defaultValues?.gfsMonthly ?? 12, + yearly: defaultValues?.gfsYearly ?? 3, + }, + }, + }); + + const mutation = useMutation({ + mutationFn: async (payload: RetentionSettings) => + await updateOrCreateBackupRetentionPolicyAction({ + databaseId: database.id, + settings: payload, + }), + onSuccess: () => { + toast.success("Retention policy updated successfully."); + router.refresh() + }, + onError: () => { + toast.error("An error occurred while updating retention policy."); + }, + }); + + const calculateTotalFiles = (values: RetentionSettings) => { + if (values.type === "gfs") { + return values.gfs.daily + values.gfs.weekly + values.gfs.monthly + values.gfs.yearly; + } + return values.type === "count" ? values.count : values.days; + }; + + const getStorageEstimate = (totalFiles: number) => { + if (totalFiles <= 10) return "Low"; + if (totalFiles <= 30) return "Medium"; + return "High"; + }; + + return ( +
+
+
{ + await mutation.mutateAsync(values); + }} + > + ( + + Retention Policy Type + + +
+ +
+ + Keep last N backups + +

+ Simple count-based retention (e.g., keep last 10 backups) +

+
+
+ +
+ +
+ + Keep backups for X days + +

+ Time-based retention (e.g., keep backups for 30 days) +

+
+
+ +
+ +
+ + GFS Rotation + + Recommended + + +

+ Grandfather-Father-Son rotation for enterprise/critical systems +

+
+
+
+
+
+ )} + /> + + + + {/* Count config */} + {form.watch("type") === "count" && ( + ( + + Number of backups to keep + + + + + Older backups beyond this count will be automatically deleted. + + + + )} + /> + )} + + {/* Days config */} + {form.watch("type") === "days" && ( + ( + + Retention period (days) + + + + + Backups older than {field.value} days will be automatically deleted. + + + + )} + /> + )} + + {/* GFS config */} + {form.watch("type") === "gfs" && ( +
+ ( + + Daily backups + + + + Keep last N daily backups + + + )} + /> + ( + + Weekly backups + + + + Keep N weekly backups + + + )} + /> + ( + + Monthly backups + + + + Keep N monthly backups + + + )} + /> + ( + + Yearly backups + + + + Keep N yearly backups + + + )} + /> +
+ )} + + + + {/* Summary */} +
+
+
+ + Storage Impact Summary +
+ {(() => { + const totalFiles = calculateTotalFiles(form.getValues()); + const estimate = getStorageEstimate(totalFiles); + return ( + + {estimate} Usage + + ); + })()} +
+
+
+ Estimated files per database: +

{calculateTotalFiles(form.getValues())} backup files

+
+
+ Policy type: +

+ {form.watch("type") === "gfs" + ? "GFS Rotation" + : form.watch("type") === "count" + ? "Count-based" + : "Time-based"} +

+
+
+
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.schema.ts b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.schema.ts new file mode 100644 index 00000000..150eeeb1 --- /dev/null +++ b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.schema.ts @@ -0,0 +1,17 @@ +import {z} from "zod"; + +const GFSSettingsSchema = z.object({ + daily: z.number().min(1).max(31), + weekly: z.number().min(0).max(52), + monthly: z.number().min(0).max(120), + yearly: z.number().min(0).max(50), +}); + +export const RetentionSettingsSchema = z.object({ + type: z.enum(["count", "days", "gfs"]), + count: z.number().min(1).max(100), + days: z.number().min(1).max(3650), + gfs: GFSSettingsSchema, +}); + +export type RetentionSettings = z.infer; \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx b/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx index f85dab20..2f534657 100644 --- a/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx +++ b/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx @@ -4,7 +4,10 @@ import {Database, DatabaseZap} from "lucide-react"; import { BackupRetentionSettings } from "@/components/wrappers/dashboard/database/retention-policy/backup-retention-settings"; -import {Database as DbSchema} from "@/db/schema/07_database"; +import {DatabaseWith as DbSchema, RetentionPolicy} from "@/db/schema/07_database"; +import { + BackupRetentionSettingsForm +} from "@/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form"; type RetentionPolicySheetProps = { database: DbSchema @@ -32,7 +35,10 @@ export const RetentionPolicySheet = (props: RetentionPolicySheetProps) => { enterprise GFS rotation strategies. - + + + + {/**/} )