mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: also add for restoration
This commit is contained in:
+3
@@ -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)],
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user