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({
|
const restorations = await db.query.restoration.findMany({
|
||||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||||
|
with: {
|
||||||
|
logs: true
|
||||||
|
},
|
||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db as dbClient, db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
|
||||||
import {logger} from "@/lib/logger";
|
import {logger} from "@/lib/logger";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {JobLogEntry} from "@/features/logs/types";
|
||||||
|
|
||||||
const log = logger.child({module: "api/agent/restore"});
|
const log = logger.child({module: "api/agent/restore"});
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
status: string
|
status: string
|
||||||
|
logs: JobLogEntry[]
|
||||||
|
durationMs: number
|
||||||
}
|
}
|
||||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
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})
|
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [restorationUpdated] = await db
|
||||||
await db
|
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set(withUpdatedAt({status: body.status as RestorationStatus}))
|
.set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.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");
|
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,
|
"when": 1780771191196,
|
||||||
"tag": "0060_shiny_sersi",
|
"tag": "0060_shiny_sersi",
|
||||||
"breakpoints": true
|
"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", {
|
export const restoration = pgTable("restorations", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
status: statusEnum("status").default("waiting").notNull(),
|
status: statusEnum("status").default("waiting").notNull(),
|
||||||
|
durationMs: bigint("duration_ms", { mode: "number" }),
|
||||||
backupStorageId: uuid("backup_storage_id")
|
backupStorageId: uuid("backup_storage_id")
|
||||||
.references(() => backupStorage.id, {onDelete: "cascade"}),
|
.references(() => backupStorage.id, {onDelete: "cascade"}),
|
||||||
backupId: uuid("backup_id")
|
backupId: uuid("backup_id")
|
||||||
@@ -136,8 +136,6 @@ export type DatabaseWith = Database & {
|
|||||||
retentionPolicy?: RetentionPolicy | null;
|
retentionPolicy?: RetentionPolicy | null;
|
||||||
alertPolicies?: AlertPolicy[] | null;
|
alertPolicies?: AlertPolicy[] | null;
|
||||||
storagePolicies?: StoragePolicy[] | null;
|
storagePolicies?: StoragePolicy[] | null;
|
||||||
logs?: JobLog[] | null;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +143,11 @@ export type BackupWith = Backup & {
|
|||||||
restorations?: Restoration[] | null;
|
restorations?: Restoration[] | null;
|
||||||
storages?: BackupStorage[] | null;
|
storages?: BackupStorage[] | null;
|
||||||
logs?: JobLog[] | null;
|
logs?: JobLog[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RestorationWith = Restoration & {
|
||||||
|
logs?: JobLog[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {z} from "zod";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
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 {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||||
@@ -44,8 +44,11 @@ export const getDatabaseDataAction = userAction
|
|||||||
|
|
||||||
const restorations = await db.query.restoration.findMany({
|
const restorations = await db.query.restoration.findMany({
|
||||||
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
|
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
|
||||||
|
with: {
|
||||||
|
logs: true
|
||||||
|
},
|
||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||||
}) as Restoration[];
|
}) as RestorationWith[];
|
||||||
|
|
||||||
const totalBackups = backups.length;
|
const totalBackups = backups.length;
|
||||||
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {DatabaseBackupActionsModal} from "@/features/database/backup-actions-modal";
|
import {DatabaseBackupActionsModal} from "@/features/database/backup-actions-modal";
|
||||||
import {DatabaseTabs} from "@/features/database/database-tabs";
|
import {DatabaseTabs} from "@/features/database/database-tabs";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
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 {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
import {useBackupModal} from "@/features/database/backup-modal-context";
|
import {useBackupModal} from "@/features/database/backup-modal-context";
|
||||||
import {DatabaseKpi} from "@/features/database/database-kpi";
|
import {DatabaseKpi} from "@/features/database/database-kpi";
|
||||||
@@ -28,7 +28,7 @@ import {LogsModal} from "@/features/logs/logs-modal";
|
|||||||
export type DatabaseContentProps = {
|
export type DatabaseContentProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
backups: BackupWith[];
|
backups: BackupWith[];
|
||||||
restorations: Restoration[];
|
restorations: RestorationWith[];
|
||||||
isAlreadyRestore: boolean;
|
isAlreadyRestore: boolean;
|
||||||
database: DatabaseWith;
|
database: DatabaseWith;
|
||||||
activeMember: MemberWithUser;
|
activeMember: MemberWithUser;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
|
import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { StatusBadge } from "@/components/common/status-badge";
|
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 { formatLocalizedDate } from "@/utils/date-formatting";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
@@ -24,11 +24,13 @@ import { toast } from "sonner";
|
|||||||
import { TooltipCustom } from "@/components/common/tooltip-custom";
|
import { TooltipCustom } from "@/components/common/tooltip-custom";
|
||||||
import { MemberWithUser } from "@/db/schema/03_organization";
|
import { MemberWithUser } from "@/db/schema/03_organization";
|
||||||
import { ButtonWithConfirm } from "@/components/common/button-with-confirm";
|
import { ButtonWithConfirm } from "@/components/common/button-with-confirm";
|
||||||
|
import {LogsModalTrigger} from "@/features/logs/logs-modal-trigger";
|
||||||
|
import {formatDuration} from "@/utils/text";
|
||||||
|
|
||||||
export function restoreColumns(
|
export function restoreColumns(
|
||||||
isAlreadyRestore: boolean,
|
isAlreadyRestore: boolean,
|
||||||
activeMember: MemberWithUser,
|
activeMember: MemberWithUser,
|
||||||
): ColumnDef<Restoration>[] {
|
): ColumnDef<RestorationWith>[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
@@ -48,6 +50,22 @@ export function restoreColumns(
|
|||||||
return <StatusBadge status={row.getValue("status")} />;
|
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",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const deleteRestoreAction = userAction
|
|||||||
.where(and(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId)))
|
.where(and(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId)))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(drizzleDb.schemas.jobLog)
|
||||||
|
.where(eq(drizzleDb.schemas.jobLog.restorationId, parsedInput.restorationId));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -50,14 +53,16 @@ export const rerunRestorationAction = userAction
|
|||||||
)
|
)
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
||||||
try {
|
try {
|
||||||
const updateResult = await db
|
const [updatedRestoration] = await db
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "waiting"})
|
.set({status: "waiting"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))
|
.where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))
|
||||||
.returning()
|
.returning()
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const updatedRestoration = updateResult[0];
|
await db
|
||||||
|
.delete(drizzleDb.schemas.jobLog)
|
||||||
|
.where(eq(drizzleDb.schemas.jobLog.restorationId, parsedInput.restorationId));
|
||||||
|
|
||||||
if (!updatedRestoration) {
|
if (!updatedRestoration) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Minus, Plus } from "lucide-react"
|
import { Minus, Plus } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { LevelType } from "@/features/logs/types"
|
import { LevelType } from "@/features/logs/types"
|
||||||
import { JobLog } from "@/db/schema/17_job-log"
|
import { JobLog } from "@/db/schema/17_job-log"
|
||||||
import { formatLocalizedDate } from "@/utils/date-formatting"
|
import { formatLocalizedDate } from "@/utils/date-formatting"
|
||||||
@@ -19,24 +20,19 @@ function TypeBadge({ entry }: { entry: JobLog }) {
|
|||||||
const isCommand = entry.entryType === "command"
|
const isCommand = entry.entryType === "command"
|
||||||
const label = isCommand ? "Command" : levelLabel[entry.level]
|
const label = isCommand ? "Command" : levelLabel[entry.level]
|
||||||
const styles = isCommand
|
const styles = isCommand
|
||||||
? "bg-secondary text-secondary-foreground"
|
? "border-transparent bg-secondary text-secondary-foreground"
|
||||||
: entry.level === "error"
|
: entry.level === "error"
|
||||||
? "bg-destructive/20 text-destructive"
|
? "border-transparent bg-destructive/20 text-destructive"
|
||||||
: entry.level === "warn"
|
: 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"
|
: entry.level === "debug"
|
||||||
? "bg-muted text-muted-foreground"
|
? "border-transparent bg-muted text-muted-foreground"
|
||||||
: "bg-sky-500/20 text-sky-600 dark:text-sky-300"
|
: "border-transparent bg-sky-500/20 text-sky-600 dark:text-sky-300"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<Badge variant="outline" className={cn("rounded-full", styles)}>
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
||||||
styles,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</Badge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user