fix: also add for restoration

This commit is contained in:
Charles GTE
2026-06-07 11:15:21 +02:00
parent 2932149b92
commit 02a8f0baf9
11 changed files with 3056 additions and 28 deletions
@@ -59,6 +59,9 @@ export default async function RoutePage(props: PageParams<{
const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
with: {
logs: true
},
orderBy: (r, {desc}) => [desc(r.createdAt)],
});
+31 -5
View File
@@ -1,17 +1,20 @@
import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {db as dbClient, db} from "@/db";
import {and, eq} from "drizzle-orm";
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
import {logger} from "@/lib/logger";
import {withUpdatedAt} from "@/db/utils";
import {JobLogEntry} from "@/features/logs/types";
const log = logger.child({module: "api/agent/restore"});
export type BodyResultRestore = {
generatedId: string
status: string
logs: JobLogEntry[]
durationMs: number
}
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
@@ -60,11 +63,34 @@ export async function POST(
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
}
await db
const [restorationUpdated] = await db
.update(drizzleDb.schemas.restoration)
.set(withUpdatedAt({status: body.status as RestorationStatus}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
.set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
const logsToInsert = body.logs.map((entry) => ({
backupId: null,
restorationId: restorationUpdated.id,
loggedAt: new Date(entry.timestamp),
entryType: entry.type,
level: entry.level,
message: entry.message,
command: entry.command ?? null,
output: entry.output ?? null,
exitCode: entry.exit_code ?? null,
durationMs: entry.duration_ms ?? null,
}));
if (logsToInsert.length > 0) {
await dbClient
.insert(drizzleDb.schemas.jobLog)
.values(logsToInsert);
}
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
@@ -0,0 +1 @@
ALTER TABLE "restorations" ADD COLUMN "duration_ms" bigint;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -428,6 +428,13 @@
"when": 1780771191196,
"tag": "0060_shiny_sersi",
"breakpoints": true
},
{
"idx": 61,
"version": "7",
"when": 1780822414802,
"tag": "0061_illegal_mole_man",
"breakpoints": true
}
]
}
+5 -3
View File
@@ -68,7 +68,7 @@ export const retentionPolicy = pgTable("retention_policies", {
export const restoration = pgTable("restorations", {
id: uuid("id").primaryKey().defaultRandom(),
status: statusEnum("status").default("waiting").notNull(),
durationMs: bigint("duration_ms", { mode: "number" }),
backupStorageId: uuid("backup_storage_id")
.references(() => backupStorage.id, {onDelete: "cascade"}),
backupId: uuid("backup_id")
@@ -136,8 +136,6 @@ export type DatabaseWith = Database & {
retentionPolicy?: RetentionPolicy | null;
alertPolicies?: AlertPolicy[] | null;
storagePolicies?: StoragePolicy[] | null;
logs?: JobLog[] | null;
};
@@ -145,7 +143,11 @@ export type BackupWith = Backup & {
restorations?: Restoration[] | null;
storages?: BackupStorage[] | null;
logs?: JobLog[] | null;
};
export type RestorationWith = Restoration & {
logs?: JobLog[] | null;
};
@@ -4,7 +4,7 @@ import {z} from "zod";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {BackupWith, Restoration} from "@/db/schema/07_database";
import {BackupWith, Restoration, RestorationWith} from "@/db/schema/07_database";
import {getOrganizationChannels} from "@/db/services/notification-channel";
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
@@ -44,8 +44,11 @@ export const getDatabaseDataAction = userAction
const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
with: {
logs: true
},
orderBy: (r, {desc}) => [desc(r.createdAt)],
}) as Restoration[];
}) as RestorationWith[];
const totalBackups = backups.length;
const availableBackups = backups.filter(b => !b.deletedAt).length;
+2 -2
View File
@@ -2,7 +2,7 @@
import {DatabaseBackupActionsModal} from "@/features/database/backup-actions-modal";
import {DatabaseTabs} from "@/features/database/database-tabs";
import {Setting} from "@/db/schema/01_setting";
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database";
import {BackupWith, DatabaseWith, RestorationWith} from "@/db/schema/07_database";
import {MemberWithUser} from "@/db/schema/03_organization";
import {useBackupModal} from "@/features/database/backup-modal-context";
import {DatabaseKpi} from "@/features/database/database-kpi";
@@ -28,7 +28,7 @@ import {LogsModal} from "@/features/logs/logs-modal";
export type DatabaseContentProps = {
settings: Setting;
backups: BackupWith[];
restorations: Restoration[];
restorations: RestorationWith[];
isAlreadyRestore: boolean;
database: DatabaseWith;
activeMember: MemberWithUser;
+20 -2
View File
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
import { ReloadIcon } from "@radix-ui/react-icons";
import { StatusBadge } from "@/components/common/status-badge";
import { Restoration } from "@/db/schema/07_database";
import {Restoration, RestorationWith} from "@/db/schema/07_database";
import { formatLocalizedDate } from "@/utils/date-formatting";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
@@ -24,11 +24,13 @@ import { toast } from "sonner";
import { TooltipCustom } from "@/components/common/tooltip-custom";
import { MemberWithUser } from "@/db/schema/03_organization";
import { ButtonWithConfirm } from "@/components/common/button-with-confirm";
import {LogsModalTrigger} from "@/features/logs/logs-modal-trigger";
import {formatDuration} from "@/utils/text";
export function restoreColumns(
isAlreadyRestore: boolean,
activeMember: MemberWithUser,
): ColumnDef<Restoration>[] {
): ColumnDef<RestorationWith>[] {
return [
{
accessorKey: "id",
@@ -48,6 +50,22 @@ export function restoreColumns(
return <StatusBadge status={row.getValue("status")} />;
},
},
{
accessorKey: "durationMs",
header: "Duration",
cell: ({row}) => {
const durationMs = row.getValue("durationMs");
return durationMs ? formatDuration(row.getValue("durationMs")) : "-"
},
},
{
accessorKey: "logs",
header: "Logs",
cell: ({row}) => {
const logs = row.original.logs ?? [];
return <LogsModalTrigger logs={logs}/>;
},
},
{
id: "actions",
cell: ({ row }) => {
+7 -2
View File
@@ -21,6 +21,9 @@ export const deleteRestoreAction = userAction
.where(and(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId)))
.execute();
await db
.delete(drizzleDb.schemas.jobLog)
.where(eq(drizzleDb.schemas.jobLog.restorationId, parsedInput.restorationId));
return {
success: true,
@@ -50,14 +53,16 @@ export const rerunRestorationAction = userAction
)
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
try {
const updateResult = await db
const [updatedRestoration] = await db
.update(drizzleDb.schemas.restoration)
.set({status: "waiting"})
.where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))
.returning()
.execute();
const updatedRestoration = updateResult[0];
await db
.delete(drizzleDb.schemas.jobLog)
.where(eq(drizzleDb.schemas.jobLog.restorationId, parsedInput.restorationId));
if (!updatedRestoration) {
return {
+8 -12
View File
@@ -3,6 +3,7 @@
import { useState } from "react"
import { Minus, Plus } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { LevelType } from "@/features/logs/types"
import { JobLog } from "@/db/schema/17_job-log"
import { formatLocalizedDate } from "@/utils/date-formatting"
@@ -19,24 +20,19 @@ function TypeBadge({ entry }: { entry: JobLog }) {
const isCommand = entry.entryType === "command"
const label = isCommand ? "Command" : levelLabel[entry.level]
const styles = isCommand
? "bg-secondary text-secondary-foreground"
? "border-transparent bg-secondary text-secondary-foreground"
: entry.level === "error"
? "bg-destructive/20 text-destructive"
? "border-transparent bg-destructive/20 text-destructive"
: entry.level === "warn"
? "bg-amber-500/20 text-amber-600 dark:text-amber-300"
? "border-transparent bg-amber-500/20 text-amber-600 dark:text-amber-300"
: entry.level === "debug"
? "bg-muted text-muted-foreground"
: "bg-sky-500/20 text-sky-600 dark:text-sky-300"
? "border-transparent bg-muted text-muted-foreground"
: "border-transparent bg-sky-500/20 text-sky-600 dark:text-sky-300"
return (
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
styles,
)}
>
<Badge variant="outline" className={cn("rounded-full", styles)}>
{label}
</span>
</Badge>
)
}