Working on the retention system.

This commit is contained in:
charlesgauthereau
2025-08-27 21:38:58 +02:00
parent 2f9e33f4db
commit 0f7b3eae11
6 changed files with 130 additions and 109 deletions
@@ -13,6 +13,7 @@ import * as drizzleDb from "@/db";
import {getOrganizationProjectDatabases} from "@/lib/services"; import {getOrganizationProjectDatabases} from "@/lib/services";
import {getOrganization} from "@/lib/auth/auth"; import {getOrganization} from "@/lib/auth/auth";
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
import {capitalizeFirstLetter} from "@/utils/text";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
projectId: string; projectId: string;
@@ -87,7 +88,7 @@ export default async function RoutePage(props: PageParams<{
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex items-center">
{dbItem.name} {capitalizeFirstLetter(dbItem.name)}
<EditButton/> <EditButton/>
<CronButton database={dbItem}/> <CronButton database={dbItem}/>
<RetentionPolicySheet/> <RetentionPolicySheet/>
@@ -1,23 +1,27 @@
import { PageParams } from "@/types/next"; import {PageParams} from "@/types/next";
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page"; import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import { buttonVariants } from "@/components/ui/button"; import {buttonVariants} from "@/components/ui/button";
import { GearIcon } from "@radix-ui/react-icons"; import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link"; import Link from "next/link";
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project"; import {
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination"; ButtonDeleteProject
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/project-card/project-database-card"; } from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import {notFound, redirect} from "next/navigation"; import {notFound, redirect} from "next/navigation";
import { db } from "@/db"; import {db} from "@/db";
import { eq } from "drizzle-orm"; import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth"; import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
projectId: string projectId: string
}>) { }>) {
const { const {
projectId } = await props.params; projectId
} = await props.params;
const organization = await getOrganization({}); const organization = await getOrganization({});
if (!organization) { if (!organization) {
@@ -30,7 +34,11 @@ export default async function RoutePage(props: PageParams<{
if (!org) notFound(); if (!org) notFound();
const proj = await db.query.project.findFirst({ const proj = await db.query.project.findFirst({
where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))), where: (proj, {
and,
eq,
not
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
with: { with: {
databases: true, databases: true,
}, },
@@ -45,13 +53,13 @@ export default async function RoutePage(props: PageParams<{
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex items-center">
{proj.name} {capitalizeFirstLetter(proj.name)}
<Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/projects/${proj.id}/edit`}> <Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7" /> <GearIcon className="w-7 h-7"/>
</Link> </Link>
</PageTitle> </PageTitle>
<PageActions className="justify-between"> <PageActions className="justify-between">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"} /> <ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</PageActions> </PageActions>
</div> </div>
+75 -73
View File
@@ -11,119 +11,121 @@ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
} }
function SheetTrigger({ function SheetTrigger({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
} }
function SheetClose({ function SheetClose({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
} }
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
} }
function SheetOverlay({ function SheetOverlay({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return ( return (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className
)} )}
{...props} {...props}
/> />
) )
} }
function SheetContent({ function SheetContent({
className, className,
children, children,
side = "right", side = "right",
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: "top" | "right" | "bottom" | "left"
}) { }) {
return ( return (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <SheetPrimitive.Content
data-slot="sheet-content" data-slot="sheet-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" && side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l",
side === "left" && // "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", side === "left" &&
side === "top" && "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r",
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", // "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "bottom" && side === "top" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
className side === "bottom" &&
)} "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
{...props} className
> )}
{children} {...props}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> >
<XIcon className="size-4" /> {children}
<span className="sr-only">Close</span> <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
</SheetPrimitive.Close> <XIcon className="size-4" />
</SheetPrimitive.Content> <span className="sr-only">Close</span>
</SheetPortal> </SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
) )
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-header" data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
) )
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-footer" data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) )
} }
function SheetTitle({ function SheetTitle({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) { }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
data-slot="sheet-title" data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
) )
} }
function SheetDescription({ function SheetDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return ( return (
<SheetPrimitive.Description <SheetPrimitive.Description
data-slot="sheet-description" data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) )
} }
@@ -69,18 +69,9 @@ export function BackupRetentionSettings() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card> <div className="flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-balance"> <div className="px-3 space-y-6">
<Database className="h-5 w-5" />
Backup Retention Policy
</CardTitle>
<CardDescription className="text-pretty">
Configure how long to keep your .dump backup files. Choose from simple count-based, time-based, or
enterprise GFS rotation strategies.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<Label className="text-sm font-medium">Retention Policy Type</Label> <Label className="text-sm font-medium">Retention Policy Type</Label>
<RadioGroup <RadioGroup
@@ -314,8 +305,8 @@ export function BackupRetentionSettings() {
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
{isLoading ? "Saving Policy..." : "Save Retention Policy"} {isLoading ? "Saving Policy..." : "Save Retention Policy"}
</Button> </Button>
</CardContent> </div>
</Card> </div>
</div> </div>
) )
} }
@@ -1,6 +1,9 @@
import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger} from "@/components/ui/sheet"; import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger} from "@/components/ui/sheet";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import {DatabaseZap} from "lucide-react"; import {Database, DatabaseZap} from "lucide-react";
import {
BackupRetentionSettings
} from "@/components/wrappers/dashboard/database/retention-policy/backup-retention-settings";
export const RetentionPolicySheet = () => { export const RetentionPolicySheet = () => {
@@ -11,14 +14,20 @@ export const RetentionPolicySheet = () => {
<DatabaseZap/> <DatabaseZap/>
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px]"> <SheetContent
className="flex gap-4 p-4 w-[540px] sm:w-[800px] max-w-[800px] max-h-screen overflow-y-scroll"
>
<SheetHeader> <SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle> <SheetTitle className="flex items-center gap-2 text-balance">
<SheetDescription> <Database className="h-5 w-5" />
This action cannot be undone. This will permanently delete your account Backup Retention Policy
and remove your data from our servers. </SheetTitle>
<SheetDescription className="text-pretty">
Configure how long to keep your .dump backup files. Choose from simple count-based, time-based, or
enterprise GFS rotation strategies.
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<BackupRetentionSettings/>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) )
+10
View File
@@ -0,0 +1,10 @@
export function truncateWords(text: string, wordLimit: number = 10): string {
if (!text) return "";
const words = text.trim().split(/\s+/);
if (words.length <= wordLimit) return text;
return words.slice(0, wordLimit).join(" ") + "…";
}
export function capitalizeFirstLetter(text: string): string {
return text ? text.charAt(0).toUpperCase() + text.slice(1) : "";
}