mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: bulk restore/backup
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user