fix: bulk restore/backup

This commit is contained in:
Charles GTE
2026-07-05 13:50:33 +02:00
parent 96c51a08c6
commit 503d5469d4
5 changed files with 22 additions and 21 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ export const DatabaseCard = (props: DatabaseCardProps) => {
<Card
className={cn(
"relative h-full flex flex-col p-4 transition-all border-border/50 bg-card hover:bg-accent/50 hover:border-primary/50 group-hover:shadow-lg overflow-hidden gap-0",
selected && "ring-2 ring-primary border-primary",
selected && "ring-1 ring-primary border-primary",
)}>
{selectable && (
<button
@@ -8,8 +8,8 @@ import {userAction} from "@/lib/safe-actions/actions";
import {assertDatabasesInOrgProject} from "@/features/database/actions/bulk-restore.action";
const bulkSchema = z.object({
projectId: z.string().uuid(),
databaseIds: z.array(z.string().uuid()).min(1),
projectId: z.uuid(),
databaseIds: z.array(z.uuid()).min(1),
});
export const bulkBackupAction = userAction
@@ -18,11 +18,9 @@ export const bulkBackupAction = userAction
try {
const {projectId, databaseIds} = parsedInput;
await assertDatabasesInOrgProject(projectId, databaseIds);
await db
.insert(drizzleDb.schemas.backup)
.values(databaseIds.map((databaseId) => ({databaseId, status: "waiting" as const})));
return {
success: true,
value: {queued: databaseIds.length},
@@ -8,7 +8,6 @@ import {ServerActionResult} from "@/types/action-type";
import {userAction, ActionError} from "@/lib/safe-actions/actions";
import {getOrganization} from "@/lib/auth/auth";
// Throws if the project isn't in the active org, or any db isn't in the project.
export async function assertDatabasesInOrgProject(projectId: string, databaseIds: string[]) {
const organization = await getOrganization({});
if (!organization) throw new ActionError("No active organization.");
@@ -33,11 +32,10 @@ export type RestorePreviewRow = {
name: string;
backupId?: string;
backupStorageId?: string;
backupDate?: string; // ISO
backupDate?: string;
restorable: boolean;
};
// For each db, latest non-deleted backup whose latest non-deleted backupStorage has status "success".
async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> {
const project = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, projectId),
@@ -84,8 +82,8 @@ async function resolveLatestRestorable(projectId: string, databaseIds: string[])
}
const bulkSchema = z.object({
projectId: z.string().uuid(),
databaseIds: z.array(z.string().uuid()).min(1),
projectId: z.uuid(),
databaseIds: z.array(z.uuid()).min(1),
});
export const bulkRestorePreviewAction = userAction
@@ -14,18 +14,23 @@ export type BulkActionBarProps = {
export const BulkActionBar = ({count, isPending, onBackup, onRestore, onClear}: BulkActionBarProps) => {
if (count === 0) return null;
return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 rounded-xl border border-primary/50 bg-card shadow-lg px-4 py-3">
<span className="rounded-full bg-primary text-primary-foreground text-xs font-bold px-3 py-1">
{count} selected
<div
className="fixed bottom-4 sm:bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 sm:gap-3 rounded-xl border border-primary/50 bg-card shadow-lg px-3 sm:px-4 py-2 sm:py-3 max-w-[calc(100vw-1.5rem)]">
<span className="flex-shrink-0 rounded-full bg-primary text-primary-foreground text-xs font-bold px-2.5 sm:px-3 py-1">
{count}
<span className="hidden sm:inline"> selected</span>
</span>
<Button size="sm" onClick={onBackup} disabled={isPending}>
{isPending ? <Loader2 className="animate-spin" /> : <DatabaseZap />} Backup
<Button size="sm" variant="outline" onClick={onBackup} disabled={isPending} title="Backup">
{isPending ? <Loader2 className="animate-spin"/> : <DatabaseZap/>}
<span className="hidden sm:inline">Backup</span>
</Button>
<Button size="sm" variant="destructive" onClick={onRestore} disabled={isPending}>
<RotateCcw /> Restore latest
<Button size="sm" variant="outline" onClick={onRestore} disabled={isPending} title="Restore latest">
<RotateCcw/>
<span className="hidden sm:inline">Restore latest</span>
</Button>
<Button size="sm" variant="ghost" onClick={onClear} disabled={isPending}>
<X /> Clear
<Button size="sm" variant="ghost" onClick={onClear} disabled={isPending} title="Clear">
<X/>
<span className="hidden sm:inline">Clear</span>
</Button>
</div>
);
@@ -29,7 +29,7 @@ export const BulkRestoreModal = (props: BulkRestoreModalProps) => {
<Dialog open={open} onOpenChange={(o) => (!o ? onCancel() : undefined)}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-destructive">
<DialogTitle>
Restore {rows.length} database(s) to latest backup
</DialogTitle>
</DialogHeader>
@@ -73,7 +73,7 @@ export const BulkRestoreModal = (props: BulkRestoreModalProps) => {
<DialogFooter>
<Button variant="outline" onClick={onCancel} disabled={submitting}>Cancel</Button>
<Button variant="destructive" onClick={onConfirm} disabled={!canConfirm}>
<Button onClick={onConfirm} disabled={!canConfirm}>
{submitting && <Loader2 className="animate-spin" />}
{restorableCount > 0 ? `Restore ${restorableCount} database(s)` : "Nothing to restore"}
</Button>