Patching a bug when delete an organization, add refetch.

This commit is contained in:
charlesgauthereau
2025-08-27 21:16:13 +02:00
parent dd7a65262c
commit 2f9e33f4db
16 changed files with 1842 additions and 38 deletions
@@ -1,18 +1,31 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Form } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form";
import {Button} from "@/components/ui/button";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input";
import { updateEmailSettingsAction } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import {
EmailFormSchema,
EmailFormType
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {
updateEmailSettingsAction
} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
export type EmailFormProps = {
defaultValues?: EmailFormType;
@@ -27,7 +40,7 @@ export const EmailForm = (props: EmailFormProps) => {
const mutation = useMutation({
mutationFn: async (values: EmailFormType) => {
const updateEmailSettings = await updateEmailSettingsAction({ name: "system", data: values });
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values});
const data = updateEmailSettings?.data?.data;
if (updateEmailSettings?.serverError || !data) {
toast.error(updateEmailSettings?.serverError);
@@ -53,14 +66,14 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpFrom"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>From Email *</FormLabel>
<FormControl>
<Input placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email from where the email will be send"}</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -68,14 +81,14 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpHost"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Server Host *</FormLabel>
<FormControl>
<Input placeholder={"ssl0.ovh.net"} {...field} />
</FormControl>
<FormDescription>{"Your email server host"}</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -83,14 +96,14 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpPort"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Server Port *</FormLabel>
<FormControl>
<Input placeholder={"465"} {...field} />
</FormControl>
<FormDescription>{"Your email server port (send)"}</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -99,7 +112,7 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpPassword"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
@@ -107,7 +120,7 @@ export const EmailForm = (props: EmailFormProps) => {
</FormControl>
<FormDescription>{"Your email server password"}</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -115,14 +128,14 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpUser"
defaultValue=""
render={({ field }) => (
render={({field}) => (
<FormItem>
<FormLabel>User Email *</FormLabel>
<FormControl>
<Input placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email server user"}</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
)}
/>
@@ -7,6 +7,7 @@ import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
import { render } from "@react-email/render";
import { toast } from "sonner";
import {Setting} from "@/db/schema/00_setting";
import {EmailFormType} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
export type SettingsEmailTabProps = {
settings: Setting;
@@ -53,7 +54,7 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
)}
</div>
<div className="mt-5">
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null} />
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined } />
</div>
</div>
);
@@ -12,6 +12,7 @@ import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
export type S3FormProps = {
defaultValues?: S3FormType;
@@ -86,7 +87,7 @@ export const StorageS3Form = (props: S3FormProps) => {
<FormItem>
<FormLabel>Secret Key *</FormLabel>
<FormControl>
<Input placeholder={"The secret key token"} {...field} />
<PasswordInput placeholder={"The secret key token"} {...field} />
</FormControl>
<FormDescription>{"Add your secret key"}</FormDescription>
<FormMessage />
@@ -0,0 +1,321 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Clock, Database, Save, Settings, Calendar, RotateCcw } from "lucide-react"
import {toast} from "sonner";
type RetentionPolicyType = "count" | "days" | "gfs"
interface GFSSettings {
daily: number
weekly: number
monthly: number
yearly: number
}
interface RetentionSettings {
type: RetentionPolicyType
count: number
days: number
gfs: GFSSettings
}
export function BackupRetentionSettings() {
const [settings, setSettings] = useState<RetentionSettings>({
type: "gfs",
count: 7,
days: 30,
gfs: {
daily: 7,
weekly: 4,
monthly: 12,
yearly: 3,
},
})
const [isLoading, setIsLoading] = useState(false)
const handleSave = async () => {
setIsLoading(true)
try {
await new Promise((resolve) => setTimeout(resolve, 1000))
toast.success("Your backup retention settings have been saved successfully.")
} catch (error) {
toast.error("Failed to save retention settings")
} finally {
setIsLoading(false)
}
}
const calculateTotalFiles = () => {
if (settings.type === "gfs") {
return settings.gfs.daily + settings.gfs.weekly + settings.gfs.monthly + settings.gfs.yearly
}
return settings.type === "count" ? settings.count : Math.ceil(settings.days / 1) // Assuming daily backups
}
const getStorageEstimate = () => {
const totalFiles = calculateTotalFiles()
if (totalFiles <= 10) return "Low"
if (totalFiles <= 30) return "Medium"
return "High"
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-balance">
<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">
<Label className="text-sm font-medium">Retention Policy Type</Label>
<RadioGroup
value={settings.type}
onValueChange={(value: RetentionPolicyType) => setSettings((prev) => ({ ...prev, type: value }))}
className="grid grid-cols-1 gap-4"
>
<div className="flex items-center space-x-3 rounded-lg border p-4 hover:bg-muted/50 transition-colors">
<RadioGroupItem value="count" id="count" />
<div className="flex-1">
<Label htmlFor="count" className="font-medium cursor-pointer">
Keep last N backups
</Label>
<p className="text-sm text-muted-foreground">
Simple count-based retention (e.g., keep last 10 backups)
</p>
</div>
</div>
<div className="flex items-center space-x-3 rounded-lg border p-4 hover:bg-muted/50 transition-colors">
<RadioGroupItem value="days" id="days" />
<div className="flex-1">
<Label htmlFor="days" className="font-medium cursor-pointer">
Keep backups for X days
</Label>
<p className="text-sm text-muted-foreground">Time-based retention (e.g., keep backups for 30 days)</p>
</div>
</div>
<div className="flex items-center space-x-3 rounded-lg border p-4 hover:bg-muted/50 transition-colors bg-accent/5">
<RadioGroupItem value="gfs" id="gfs" />
<div className="flex-1">
<Label htmlFor="gfs" className="font-medium cursor-pointer flex items-center gap-2">
GFS Rotation
<Badge variant="secondary" className="text-xs">
Recommended
</Badge>
</Label>
<p className="text-sm text-muted-foreground">
Grandfather-Father-Son rotation for enterprise/critical systems
</p>
</div>
</div>
</RadioGroup>
</div>
<Separator />
{settings.type === "count" && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<Label className="font-medium">Count-Based Configuration</Label>
</div>
<div className="space-y-2">
<Label htmlFor="backup-count">Number of backups to keep</Label>
<Input
id="backup-count"
type="number"
min="1"
max="100"
value={settings.count}
onChange={(e) => setSettings((prev) => ({ ...prev, count: Number.parseInt(e.target.value) || 1 }))}
className="w-32"
/>
<p className="text-xs text-muted-foreground">
Older backups beyond this count will be automatically deleted
</p>
</div>
</div>
)}
{settings.type === "days" && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<Label className="font-medium">Time-Based Configuration</Label>
</div>
<div className="space-y-2">
<Label htmlFor="retention-days">Retention period (days)</Label>
<Input
id="retention-days"
type="number"
min="1"
max="3650"
value={settings.days}
onChange={(e) => setSettings((prev) => ({ ...prev, days: Number.parseInt(e.target.value) || 1 }))}
className="w-32"
/>
<p className="text-xs text-muted-foreground">
Backups older than {settings.days} days will be automatically deleted
</p>
</div>
</div>
)}
{settings.type === "gfs" && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<RotateCcw className="h-4 w-4 text-muted-foreground" />
<Label className="font-medium">GFS Rotation Configuration</Label>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="daily-backups">Daily backups</Label>
<Input
id="daily-backups"
type="number"
min="1"
max="31"
value={settings.gfs.daily}
onChange={(e) =>
setSettings((prev) => ({
...prev,
gfs: { ...prev.gfs, daily: Number.parseInt(e.target.value) || 1 },
}))
}
/>
<p className="text-xs text-muted-foreground">Keep last N daily backups</p>
</div>
<div className="space-y-2">
<Label htmlFor="weekly-backups">Weekly backups</Label>
<Input
id="weekly-backups"
type="number"
min="0"
max="52"
value={settings.gfs.weekly}
onChange={(e) =>
setSettings((prev) => ({
...prev,
gfs: { ...prev.gfs, weekly: Number.parseInt(e.target.value) || 0 },
}))
}
/>
<p className="text-xs text-muted-foreground">Keep N weekly backups</p>
</div>
<div className="space-y-2">
<Label htmlFor="monthly-backups">Monthly backups</Label>
<Input
id="monthly-backups"
type="number"
min="0"
max="120"
value={settings.gfs.monthly}
onChange={(e) =>
setSettings((prev) => ({
...prev,
gfs: { ...prev.gfs, monthly: Number.parseInt(e.target.value) || 0 },
}))
}
/>
<p className="text-xs text-muted-foreground">Keep N monthly backups</p>
</div>
<div className="space-y-2">
<Label htmlFor="yearly-backups">Yearly backups</Label>
<Input
id="yearly-backups"
type="number"
min="0"
max="50"
value={settings.gfs.yearly}
onChange={(e) =>
setSettings((prev) => ({
...prev,
gfs: { ...prev.gfs, yearly: Number.parseInt(e.target.value) || 0 },
}))
}
/>
<p className="text-xs text-muted-foreground">Keep N yearly backups</p>
</div>
</div>
<div className="bg-muted/50 p-3 rounded-lg">
<p className="text-sm font-medium mb-1">GFS Strategy Benefits:</p>
<ul className="text-xs text-muted-foreground space-y-1">
<li> Balanced storage usage with long-term retention</li>
<li> Automatic promotion of backups (daily weekly monthly yearly)</li>
<li> Industry standard for enterprise backup systems</li>
</ul>
</div>
</div>
)}
<Separator />
<div className="rounded-lg border p-4 space-y-3 bg-card">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Storage Impact Summary</span>
</div>
<Badge
variant={
getStorageEstimate() === "Low"
? "default"
: getStorageEstimate() === "Medium"
? "secondary"
: "destructive"
}
>
{getStorageEstimate()} Usage
</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Estimated files per database:</span>
<p className="font-medium">{calculateTotalFiles()} backup files</p>
</div>
<div>
<span className="text-muted-foreground">Policy type:</span>
<p className="font-medium capitalize">
{settings.type === "gfs" ? "GFS Rotation" : settings.type === "count" ? "Count-based" : "Time-based"}
</p>
</div>
</div>
{settings.type === "gfs" && (
<div className="text-xs text-muted-foreground bg-accent/10 p-2 rounded border border-accent/20">
<strong>Note:</strong> With GFS rotation, you'll have approximately {calculateTotalFiles()} files per
database per year, providing excellent long-term retention with optimized storage usage.
</div>
)}
</div>
<Button onClick={handleSave} disabled={isLoading} className="w-full">
<Save className="h-4 w-4 mr-2" />
{isLoading ? "Saving Policy..." : "Save Retention Policy"}
</Button>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,25 @@
import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger} from "@/components/ui/sheet";
import {Button} from "@/components/ui/button";
import {DatabaseZap} from "lucide-react";
export const RetentionPolicySheet = () => {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">
<DatabaseZap/>
</Button>
</SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px]">
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
)
}