mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on retention policy and cron system.
This commit is contained in:
+13
-11
@@ -30,7 +30,6 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const databasesProject = await getOrganizationProjectDatabases({
|
const databasesProject = await getOrganizationProjectDatabases({
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
projectId: projectId
|
projectId: projectId
|
||||||
@@ -64,18 +63,20 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const [totalBackups, successfulBackups] = await Promise.all([
|
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
db
|
|
||||||
.select({count: drizzleDb.schemas.backup.id})
|
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||||
.then((rows) => rows.length),
|
.then(rows => rows.length);
|
||||||
db
|
|
||||||
.select({count: drizzleDb.schemas.backup.id})
|
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||||
|
|
||||||
|
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(and(eq(drizzleDb.schemas.backup.databaseId, dbItem.id), eq(drizzleDb.schemas.backup.status, "success")))
|
.where(and(
|
||||||
.then((rows) => rows.length),
|
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||||
]);
|
eq(drizzleDb.schemas.backup.status, "success")
|
||||||
|
))
|
||||||
|
.then(rows => rows.length);
|
||||||
|
|
||||||
|
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
@@ -101,7 +102,8 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
<DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups}/>
|
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||||
|
totalBackups={totalBackups}/>
|
||||||
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
||||||
backups={backups}
|
backups={backups}
|
||||||
restorations={restorations}/>
|
restorations={restorations}/>
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ services:
|
|||||||
|
|
||||||
s3:
|
s3:
|
||||||
container_name: s3-portabase-dev
|
container_name: s3-portabase-dev
|
||||||
image: docker.io/bitnami/minio:latest
|
image: minio/minio:latest
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9001:9001"
|
- "9001:9001"
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export const updateDatabaseBackupPolicyAction = userAction
|
|||||||
.returning()
|
.returning()
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
if (cronPolicy == null) {
|
||||||
|
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||||
|
.where(eq(drizzleDb.schemas.retentionPolicy.databaseId, parsedInput.databaseId)).execute();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: updated,
|
data: updated,
|
||||||
};
|
};
|
||||||
|
|||||||
+106
-108
@@ -9,11 +9,10 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
useZodForm
|
useZodForm
|
||||||
} from "@/components/ui/form";
|
} 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 {RetentionSettings, RetentionSettingsSchema} from "./backup-retention-settings.schema";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {updateOrCreateBackupRetentionPolicyAction} from "./backup-retention-settings.action";
|
import {updateOrCreateBackupRetentionPolicyAction} from "./backup-retention-settings.action";
|
||||||
import {Database, DatabaseWith, RetentionPolicy} from "@/db/schema/07_database";
|
import {DatabaseWith, RetentionPolicy} from "@/db/schema/07_database";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group";
|
import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group";
|
||||||
@@ -23,20 +22,16 @@ import {Input} from "@/components/ui/input";
|
|||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Calendar, Save} from "lucide-react";
|
import {Calendar, Save} from "lucide-react";
|
||||||
|
|
||||||
|
|
||||||
export type BackupRetentionSettingsFormProps = {
|
export type BackupRetentionSettingsFormProps = {
|
||||||
defaultValues?: RetentionPolicy;
|
defaultValues?: RetentionPolicy;
|
||||||
database: DatabaseWith;
|
database: DatabaseWith;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRetentionSettingsFormProps) => {
|
export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRetentionSettingsFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const form = useZodForm({
|
const defaultValuesFormatted: RetentionSettings = {
|
||||||
schema: RetentionSettingsSchema,
|
type: defaultValues?.type,
|
||||||
defaultValues: {
|
|
||||||
type: defaultValues?.type ?? "gfs",
|
|
||||||
count: defaultValues?.count ?? 7,
|
count: defaultValues?.count ?? 7,
|
||||||
days: defaultValues?.days ?? 30,
|
days: defaultValues?.days ?? 30,
|
||||||
gfs: {
|
gfs: {
|
||||||
@@ -45,7 +40,11 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
monthly: defaultValues?.gfsMonthly ?? 12,
|
monthly: defaultValues?.gfsMonthly ?? 12,
|
||||||
yearly: defaultValues?.gfsYearly ?? 3,
|
yearly: defaultValues?.gfsYearly ?? 3,
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const form = useZodForm({
|
||||||
|
schema: RetentionSettingsSchema,
|
||||||
|
defaultValues: defaultValuesFormatted,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
@@ -56,7 +55,7 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Retention policy updated successfully.");
|
toast.success("Retention policy updated successfully.");
|
||||||
router.refresh()
|
router.refresh();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error("An error occurred while updating retention policy.");
|
toast.error("An error occurred while updating retention policy.");
|
||||||
@@ -64,10 +63,15 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
});
|
});
|
||||||
|
|
||||||
const calculateTotalFiles = (values: RetentionSettings) => {
|
const calculateTotalFiles = (values: RetentionSettings) => {
|
||||||
if (values.type === "gfs") {
|
if (values.type === "gfs" && values.gfs) {
|
||||||
return values.gfs.daily + values.gfs.weekly + values.gfs.monthly + values.gfs.yearly;
|
return (
|
||||||
|
(values.gfs.daily ?? 0) +
|
||||||
|
(values.gfs.weekly ?? 0) +
|
||||||
|
(values.gfs.monthly ?? 0) +
|
||||||
|
(values.gfs.yearly ?? 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return values.type === "count" ? values.count : values.days;
|
return values.type === "count" ? values.count ?? 0 : values.type === "days" ? values.days ?? 0 : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStorageEstimate = (totalFiles: number) => {
|
const getStorageEstimate = (totalFiles: number) => {
|
||||||
@@ -94,61 +98,69 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
<FormLabel>Retention Policy Type</FormLabel>
|
<FormLabel>Retention Policy Type</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={field.value}
|
value={field.value ?? ""}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
className="grid grid-cols-1 gap-4"
|
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"/>
|
id: "count",
|
||||||
|
label: "Keep last N backups",
|
||||||
|
desc: "Simple count-based retention (e.g., keep last 10 backups)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "days",
|
||||||
|
label: "Keep backups for X days",
|
||||||
|
desc: "Time-based retention (e.g., keep backups for 30 days)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gfs",
|
||||||
|
label: "GFS Rotation",
|
||||||
|
desc: "Grandfather-Father-Son rotation for enterprise/critical systems",
|
||||||
|
badge: "Recommended",
|
||||||
|
},
|
||||||
|
].map((opt) => (
|
||||||
|
<FormLabel
|
||||||
|
key={opt.id}
|
||||||
|
htmlFor={opt.id}
|
||||||
|
className={`flex items-center space-x-3 rounded-lg border p-4 transition-colors cursor-pointer ${
|
||||||
|
field.value === opt.id
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "hover:bg-muted/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RadioGroupItem value={opt.id} id={opt.id}/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormLabel htmlFor="count" className="font-medium cursor-pointer">
|
<span className="font-medium flex items-center gap-2">
|
||||||
Keep last N backups
|
{opt.label}
|
||||||
</FormLabel>
|
{opt.badge && (
|
||||||
<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">
|
|
||||||
<FormLabel htmlFor="days" className="font-medium cursor-pointer">
|
|
||||||
Keep backups for X days
|
|
||||||
</FormLabel>
|
|
||||||
<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">
|
|
||||||
<FormLabel htmlFor="gfs"
|
|
||||||
className="font-medium cursor-pointer flex items-center gap-2">
|
|
||||||
GFS Rotation
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
Recommended
|
{opt.badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-muted-foreground">{opt.desc}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{database.retentionPolicy?.type === opt.id && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Actual
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<p className="text-sm text-muted-foreground">
|
))}
|
||||||
Grandfather-Father-Son rotation for enterprise/critical systems
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{form.watch("type") && (
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Count config */}
|
|
||||||
{form.watch("type") === "count" && (
|
{form.watch("type") === "count" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -157,7 +169,14 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Number of backups to keep</FormLabel>
|
<FormLabel>Number of backups to keep</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" min={1} max={100} className="w-32" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
className="w-32"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Older backups beyond this count will be automatically deleted.
|
Older backups beyond this count will be automatically deleted.
|
||||||
@@ -168,7 +187,6 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Days config */}
|
|
||||||
{form.watch("type") === "days" && (
|
{form.watch("type") === "days" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -177,7 +195,14 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Retention period (days)</FormLabel>
|
<FormLabel>Retention period (days)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" min={1} max={3650} className="w-32" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={3650}
|
||||||
|
className="w-32"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Backups older than {field.value} days will be automatically deleted.
|
Backups older than {field.value} days will be automatically deleted.
|
||||||
@@ -188,71 +213,41 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GFS config */}
|
|
||||||
{form.watch("type") === "gfs" && (
|
{form.watch("type") === "gfs" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{["daily", "weekly", "monthly", "yearly"].map((key) => (
|
||||||
<FormField
|
<FormField
|
||||||
|
key={key}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="gfs.daily"
|
name={`gfs.${key}` as const}
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Daily backups</FormLabel>
|
<FormLabel>
|
||||||
|
{key.charAt(0).toUpperCase() + key.slice(1)} backups
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" min={1} max={31} {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={key === "yearly" ? 50 : key === "monthly" ? 120 : key === "weekly" ? 52 : 31}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Keep last N daily backups</FormDescription>
|
<FormDescription>
|
||||||
<FormMessage/>
|
Keep N {key} backups
|
||||||
</FormItem>
|
</FormDescription>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="gfs.weekly"
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Weekly backups</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="number" min={0} max={52} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Keep N weekly backups</FormDescription>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="gfs.monthly"
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Monthly backups</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="number" min={0} max={120} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Keep N monthly backups</FormDescription>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="gfs.yearly"
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Yearly backups</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="number" min={0} max={50} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Keep N yearly backups</FormDescription>
|
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{form.watch("type") && (
|
||||||
|
<>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|
||||||
{/* Summary */}
|
|
||||||
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -280,7 +275,9 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Estimated files per database:</span>
|
<span className="text-muted-foreground">Estimated files per database:</span>
|
||||||
<p className="font-medium">{calculateTotalFiles(form.getValues())} backup files</p>
|
<p className="font-medium">
|
||||||
|
{calculateTotalFiles(form.getValues())} backup files
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Policy type:</span>
|
<span className="text-muted-foreground">Policy type:</span>
|
||||||
@@ -294,13 +291,14 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" disabled={mutation.isPending} className="w-full">
|
<Button type="submit" disabled={mutation.isPending} className="w-full">
|
||||||
<Save className="h-4 w-4 mr-2"/>
|
<Save className="h-4 w-4 mr-2"/>
|
||||||
{mutation.isPending ? "Saving Policy..." : "Save Retention Policy"}
|
{mutation.isPending ? "Saving Policy..." : "Save Retention Policy"}
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+6
-4
@@ -4,15 +4,17 @@ import {userAction} from "@/safe-actions"
|
|||||||
import {z} from "zod"
|
import {z} from "zod"
|
||||||
import {db} from "@/db"
|
import {db} from "@/db"
|
||||||
import {eq} from "drizzle-orm"
|
import {eq} from "drizzle-orm"
|
||||||
import {retentionSettingsSchema} from "@/components/wrappers/dashboard/database/retention-policy/schema";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {
|
||||||
|
RetentionSettingsSchema
|
||||||
|
} from "@/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.schema";
|
||||||
|
|
||||||
|
|
||||||
export const updateOrCreateBackupRetentionPolicyAction = userAction
|
export const updateOrCreateBackupRetentionPolicyAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
databaseId: z.string(),
|
databaseId: z.string(),
|
||||||
settings: retentionSettingsSchema,
|
settings: RetentionSettingsSchema,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.action(async ({parsedInput}) => {
|
.action(async ({parsedInput}) => {
|
||||||
@@ -44,11 +46,12 @@ export const updateOrCreateBackupRetentionPolicyAction = userAction
|
|||||||
.returning()
|
.returning()
|
||||||
} else {
|
} else {
|
||||||
// Insert new policy
|
// Insert new policy
|
||||||
|
|
||||||
updated = await db
|
updated = await db
|
||||||
.insert(drizzleDb.schemas.retentionPolicy)
|
.insert(drizzleDb.schemas.retentionPolicy)
|
||||||
.values({
|
.values({
|
||||||
databaseId,
|
databaseId,
|
||||||
type: settings.type,
|
type: settings.type ?? "gfs",
|
||||||
count: settings.count,
|
count: settings.count,
|
||||||
days: settings.days,
|
days: settings.days,
|
||||||
gfsDaily: settings.gfs.daily,
|
gfsDaily: settings.gfs.daily,
|
||||||
@@ -58,7 +61,6 @@ export const updateOrCreateBackupRetentionPolicyAction = userAction
|
|||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: updated[0],
|
data: updated[0],
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ const GFSSettingsSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const RetentionSettingsSchema = z.object({
|
export const RetentionSettingsSchema = z.object({
|
||||||
type: z.enum(["count", "days", "gfs"]),
|
type: z.enum(["count", "days", "gfs"]).optional(),
|
||||||
count: z.number().min(1).max(100),
|
count: z.number().min(1).max(100),
|
||||||
days: z.number().min(1).max(3650),
|
days: z.number().min(1).max(3650),
|
||||||
gfs: GFSSettingsSchema,
|
gfs: GFSSettingsSchema,
|
||||||
|
|||||||
+12
-8
@@ -1,9 +1,6 @@
|
|||||||
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 {Database, DatabaseZap} from "lucide-react";
|
import {Database, DatabaseZap} from "lucide-react";
|
||||||
import {
|
|
||||||
BackupRetentionSettings
|
|
||||||
} from "@/components/wrappers/dashboard/database/retention-policy/backup-retention-settings";
|
|
||||||
import {DatabaseWith as DbSchema, RetentionPolicy} from "@/db/schema/07_database";
|
import {DatabaseWith as DbSchema, RetentionPolicy} from "@/db/schema/07_database";
|
||||||
import {
|
import {
|
||||||
BackupRetentionSettingsForm
|
BackupRetentionSettingsForm
|
||||||
@@ -13,7 +10,7 @@ type RetentionPolicySheetProps = {
|
|||||||
database: DbSchema
|
database: DbSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RetentionPolicySheet = (props: RetentionPolicySheetProps) => {
|
export const RetentionPolicySheet = ({database}: RetentionPolicySheetProps) => {
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
@@ -35,10 +32,17 @@ export const RetentionPolicySheet = (props: RetentionPolicySheetProps) => {
|
|||||||
enterprise GFS rotation strategies.
|
enterprise GFS rotation strategies.
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
{database.backupPolicy !== null ?
|
||||||
<BackupRetentionSettingsForm database={props.database} defaultValues={props.database.retentionPolicy as RetentionPolicy}/>
|
<BackupRetentionSettingsForm database={database}
|
||||||
|
defaultValues={database.retentionPolicy as RetentionPolicy}/>
|
||||||
{/*<BackupRetentionSettings database={props.database}/>*/}
|
:
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center text-center py-12 gap-4 border rounded-lg">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No backup policy configured yet. Please configure one !
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
// --- Zod Schema ---
|
|
||||||
import {z} from "zod";
|
|
||||||
|
|
||||||
export 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<typeof retentionSettingsSchema>
|
|
||||||
@@ -1,29 +1,63 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
|
import {DatabaseBackup, Server, CheckCircle, Clock} from "lucide-react";
|
||||||
|
|
||||||
export type DatabaseKpiPro = {
|
export type DatabaseKpiPro = {
|
||||||
successRate: any;
|
successRate: any;
|
||||||
database: Database;
|
database: Database;
|
||||||
totalBackups: number;
|
totalBackups: number;
|
||||||
|
availableBackups: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatabaseKpi = (props: DatabaseKpiPro) => {
|
export const DatabaseKpi = (props: DatabaseKpiPro) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">Backups</CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardContent>{props.totalBackups}</CardContent>
|
<CardTitle className="text-sm font-medium">Available Backups</CardTitle>
|
||||||
|
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{props.availableBackups}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Backups currently available</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">Success rate</CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardContent>{props.successRate ? `${props.successRate} %` : "Unavailable for now."}</CardContent>
|
<CardTitle className="text-sm font-medium">Total Backups</CardTitle>
|
||||||
|
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{props.totalBackups}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Total backups recorded</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">Last contact</CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardContent>{formatDateLastContact(props.database.lastContact)}</CardContent>
|
<CardTitle className="text-sm font-medium">Success Rate</CardTitle>
|
||||||
|
<CheckCircle className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{props.successRate ? `${props.successRate.toFixed(0)} %` : "Unavailable"}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Backup success rate</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Last Contact</CardTitle>
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{formatDateLastContact(props.database.lastContact)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Time of last backup contact</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user