From 0216c40bfeaa8cf9c0bb9e8fd6c12ccefdf3e0c2 Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Sat, 17 Jan 2026 15:57:37 +0100 Subject: [PATCH] feat: Working on UI/UX for multiple storage backends. --- .../database/[databaseId]/page.tsx | 22 ++- .../projects/[projectId]/page.tsx | 6 +- app/(customer)/dashboard/layout.tsx | 1 + app/providers.tsx | 5 +- package.json | 1 + pnpm-lock.yaml | 9 + .../admin/notifications/logs/columns.tsx | 6 +- .../backup/actions/backup-actions-cell.tsx | 61 +++++++ .../backup/actions/backup-actions-form.tsx | 158 ++++++++++++++++++ .../backup/actions/backup-actions-modal.tsx | 39 +++++ .../backup/actions/backup-actions.action.ts | 81 +++++++++ .../backup/actions/backup-actions.schema.ts | 10 ++ .../database/backup/backup-modal-context.tsx | 60 +++++++ .../database/database-backup-list.tsx | 4 +- .../projects/database/database-content.tsx | 39 +++++ .../projects/database/database-tabs.tsx | 5 +- src/db/schema/07_database.ts | 9 +- src/db/schema/14_storage-backup.ts | 18 +- src/db/services/backup.ts | 19 +++ src/features/dashboard/backup/columns.tsx | 5 + src/features/storages/helpers.ts | 29 ++-- src/features/storages/providers/local.ts | 8 +- 22 files changed, 556 insertions(+), 39 deletions(-) create mode 100644 src/components/wrappers/dashboard/database/backup/actions/backup-actions-cell.tsx create mode 100644 src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx create mode 100644 src/components/wrappers/dashboard/database/backup/actions/backup-actions-modal.tsx create mode 100644 src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts create mode 100644 src/components/wrappers/dashboard/database/backup/actions/backup-actions.schema.ts create mode 100644 src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx create mode 100644 src/components/wrappers/dashboard/projects/database/database-content.tsx create mode 100644 src/db/services/backup.ts diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index b8d72a97..a2b91e42 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -2,7 +2,6 @@ import {PageParams} from "@/types/next"; import {notFound, redirect} from "next/navigation"; import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; -import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs"; import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi"; import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button"; import {db} from "@/db"; @@ -17,6 +16,8 @@ import {ImportModal} from "@/components/wrappers/dashboard/database/import/impor import {getOrganizationStorageChannels} from "@/db/services/storage-channel"; import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal"; import {HardDrive, Megaphone} from "lucide-react"; +import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content"; export default async function RoutePage(props: PageParams<{ projectId: string; @@ -54,6 +55,11 @@ export default async function RoutePage(props: PageParams<{ where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id), with: { restorations: true, + storages: { + with: { + storageChannel: true + } + } }, orderBy: (b, {desc}) => [desc(b.createdAt)], }); @@ -147,10 +153,16 @@ export default async function RoutePage(props: PageParams<{ - + + + ); diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx index 0798540e..7e8223ca 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx @@ -15,11 +15,7 @@ import {eq} from "drizzle-orm"; import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import * as drizzleDb from "@/db"; import {capitalizeFirstLetter} from "@/utils/text"; -import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; -import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button"; -import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal"; -import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal"; -import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; + export default async function RoutePage(props: PageParams<{ projectId: string diff --git a/app/(customer)/dashboard/layout.tsx b/app/(customer)/dashboard/layout.tsx index 3e683bd0..bf04ccf7 100644 --- a/app/(customer)/dashboard/layout.tsx +++ b/app/(customer)/dashboard/layout.tsx @@ -6,6 +6,7 @@ import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sid import {Header} from "@/features/layout/Header"; import {currentUser} from "@/lib/auth/current-user"; import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater"; +import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; export default async function Layout({children}: { children: ReactNode }) { const user = await currentUser(); diff --git a/app/providers.tsx b/app/providers.tsx index 8ec7abcd..4c6dbe90 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -6,6 +6,7 @@ import {ThemeProvider} from "@/features/theme/theme-provider"; import {Toaster} from "@/components/ui/sonner"; import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root"; +import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; export type ProviderProps = PropsWithChildren<{}>; @@ -21,8 +22,8 @@ export const Providers = (props: ProviderProps) => { > - - {props.children} + + {props.children} diff --git a/package.json b/package.json index bb2e2dac..1fbd16c5 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "sonner": "^2.0.3", + "swiper": "^12.0.3", "tailwind-merge": "^3.3.0", "uuid": "^11.1.0", "vaul": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8f2fba1..bf06a775 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,6 +230,9 @@ importers: sonner: specifier: ^2.0.3 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + swiper: + specifier: ^12.0.3 + version: 12.0.3 tailwind-merge: specifier: ^3.3.0 version: 3.4.0 @@ -6230,6 +6233,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swiper@12.0.3: + resolution: {integrity: sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==} + engines: {node: '>= 4.7.0'} + tailwind-merge@3.2.0: resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==} @@ -13556,6 +13563,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swiper@12.0.3: {} + tailwind-merge@3.2.0: {} tailwind-merge@3.4.0: {} diff --git a/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx b/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx index 4f44b7cd..246563cc 100644 --- a/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx +++ b/src/components/wrappers/dashboard/admin/notifications/logs/columns.tsx @@ -89,7 +89,7 @@ export function notificationLogsColumns(): ColumnDef { +export const getStatusIcon = (status: boolean) => { switch (status) { case true: return @@ -98,10 +98,12 @@ const getStatusIcon = (status: boolean) => { } } -const getStatusColor = (status: string) => { +export const getStatusColor = (status: string) => { switch (status) { case "delivered": return "bg-green-100 dark:bg-green-100/10" + case "success": + return "bg-green-100 dark:bg-green-100/10" case "failed": return "bg-red-100 dark:bg-red-100/10" case "pending": diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-cell.tsx b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-cell.tsx new file mode 100644 index 00000000..bc7a7b6e --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-cell.tsx @@ -0,0 +1,61 @@ +"use client"; + +import {BackupWith} from "@/db/schema/07_database"; +import {useBackupModal} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import {Button} from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator +} from "@/components/ui/dropdown-menu"; +import {MoreHorizontal, Trash2, Download} from "lucide-react"; +import {ReloadIcon} from "@radix-ui/react-icons"; +import {cn} from "@/lib/utils"; +import {MemberWithUser} from "@/db/schema/03_organization"; +import {useMutation} from "@tanstack/react-query"; +import {deleteBackupAction} from "@/features/dashboard/restore/restore.action"; +import {toast} from "sonner"; + +interface DatabaseActionsCellProps { + backup: BackupWith; + activeMember: MemberWithUser; +} + +export function DatabaseActionsCell({backup, activeMember}: DatabaseActionsCellProps) { + const {openModal} = useBackupModal(); + + + if (backup.deletedAt || activeMember.role === "member") return null; + + + return ( +
+ + + + + + Actions + openModal("restore", backup)}> + Restore + + openModal("download", backup)}> + Download + + + openModal("delete", backup)} className="text-red-600"> + Delete + + + +
+ + ); +} + diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx new file mode 100644 index 00000000..98756abd --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx @@ -0,0 +1,158 @@ +"use client" +import {BackupWith} from "@/db/schema/07_database"; +import React, {useState} from "react"; +import {Swiper, SwiperSlide} from "swiper/react"; + +import "swiper/css"; +import "swiper/css/pagination"; + +import {Pagination, Mousewheel} from "swiper/modules"; +import {DatabaseActionKind} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; +import { + BackupActionsSchema, + BackupActionsType +} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.schema"; +import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; +import {useMutation} from "@tanstack/react-query"; +import {BackupStorageWith} from "@/db/schema/14_storage-backup"; +import {TooltipProvider} from "@/components/ui/tooltip"; +import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; +import {truncateWords} from "@/utils/text"; +import {useIsMobile} from "@/hooks/use-mobile"; +import {Badge} from "@/components/ui/badge"; +import {getStatusColor, getStatusIcon} from "@/components/wrappers/dashboard/admin/notifications/logs/columns"; +import {downloadBackupAction} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action"; +import {toast} from "sonner"; + +type BackupActionsFormProps = { + backup: BackupWith; + action: DatabaseActionKind; +} + +export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { + const isMobile = useIsMobile(); + + const form = useZodForm({ + schema: BackupActionsSchema, + }); + + const mutation = useMutation({ + mutationFn: async (values: BackupActionsType) => { + // implement your mutation logic here + console.log(values); + + const result = await downloadBackupAction({backupStorageId: values.backupStorageId}) + + const inner = result?.data; + + console.log(inner); + + + if (inner?.success) { + toast.success(inner.actionSuccess?.message); + } else { + toast.error(inner?.actionError?.message); + } + + + }, + }); + + + return ( + + + + {action == "delete" ? + <> + + : +
{ + await mutation.mutateAsync(values); + }} + > + ( + + Choose a storage backup + +
+ + {backup.storages?.map((storage: BackupStorageWith) => ( + + + + + )) ??

No storages available

} +
+
+
+ +
+ )} + /> +
+ + Confirm + +
+ + } + +
+ ); +} diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-modal.tsx b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-modal.tsx new file mode 100644 index 00000000..a092f5b6 --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-modal.tsx @@ -0,0 +1,39 @@ +"use client" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import {Separator} from "@/components/ui/separator"; +import {BackupActionsForm} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-form"; +import { + getBackupActionTextBasedOnActionKind, + useBackupModal +} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; + + +type DatabaseActionsModalProps = {} + + +export const DatabaseBackupActionsModal = ({}: DatabaseActionsModalProps) => { + const {open, action, backup, closeModal} = useBackupModal(); + if (!backup || !action) return null; + const text = getBackupActionTextBasedOnActionKind(action); + + return ( + + + + {text} storage backup ? + + Select the backup storage you want to {text.toLowerCase()} + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts new file mode 100644 index 00000000..18f638ae --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts @@ -0,0 +1,81 @@ +"use server" +import {userAction} from "@/lib/safe-actions/actions"; +import {ServerActionResult} from "@/types/action-type"; +import {z} from "zod"; +import type {StorageInput} from "@/features/storages/types"; +import {dispatchStorage} from "@/features/storages/dispatch"; +import {db} from "@/db"; +import {eq} from "drizzle-orm"; +import * as drizzleDb from "@/db"; + + +export const downloadBackupAction = userAction.schema( + z.object({ + backupStorageId: z.string(), + }) +).action(async ({parsedInput}): Promise> => { + const {backupStorageId} = parsedInput; + try { + + const backupStorage = await db.query.backupStorage.findFirst({ + where: eq(drizzleDb.schemas.backupStorage.id, backupStorageId), + + }); + + if (!backupStorage) { + return { + success: false, + actionError: { + message: "Backup storage not found.", + status: 404, + messageParams: {backupStorageId: backupStorageId}, + }, + }; + } + + console.log(backupStorage); + + if (backupStorage.status != "success" || !backupStorage.path) { + return { + success: false, + actionError: { + message: "An error occurred.", + status: 500, + messageParams: {backupStorageId: backupStorageId}, + }, + } + } + + const input: StorageInput = { + action: "get", + data: { + path: backupStorage.path, + signedUrl: true, + }, + }; + + const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId); + + console.log(result); + + return { + success: true, + value: result.url, + actionSuccess: { + message: "Backup Storage downloaded successfully.", + messageParams: {backupStorageId: backupStorageId}, + }, + }; + } catch (error) { + console.error("Error:", error); + return { + success: false, + actionError: { + message: "Failed to get presigned url.", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: {backupStorageId: backupStorageId}, + }, + }; + } +}); \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions.schema.ts b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.schema.ts new file mode 100644 index 00000000..2dc473ce --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.schema.ts @@ -0,0 +1,10 @@ +"use client"; + +import { z } from "zod"; +import {zString} from "@/lib/zod"; + +export const BackupActionsSchema = z.object({ + backupStorageId: zString(), +}); + +export type BackupActionsType = z.infer; diff --git a/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx b/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx new file mode 100644 index 00000000..5fa81aa8 --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx @@ -0,0 +1,60 @@ +"use client"; + +import {createContext, useContext, useState, ReactNode} from "react"; +import {BackupWith} from "@/db/schema/07_database"; + +export type DatabaseActionKind = "restore" | "download" | "delete"; + +export function getBackupActionTextBasedOnActionKind(kind: DatabaseActionKind) { + switch (kind) { + case "restore": + return "Restore"; + case "download": + return "Download"; + case "delete": + return "Delete"; + default: + return "Unknown"; + } +} + + +type BackupModalContextType = { + open: boolean; + action: DatabaseActionKind | null; + backup: BackupWith | null; + openModal: (action: DatabaseActionKind, backup: BackupWith) => void; + closeModal: () => void; +}; + +const BackupModalContext = createContext(undefined); + +export const BackupModalProvider = ({children}: { children: ReactNode }) => { + const [open, setOpen] = useState(false); + const [action, setAction] = useState(null); + const [backup, setBackup] = useState(null); + + const openModal = (newAction: DatabaseActionKind, newBackup: BackupWith) => { + setAction(newAction); + setBackup(newBackup); + setOpen(true); + }; + + const closeModal = () => { + setOpen(false); + setAction(null); + setBackup(null); + }; + + return ( + + {children} + + ); +}; + +export const useBackupModal = () => { + const context = useContext(BackupModalContext); + if (!context) throw new Error("useBackupModal must be used within BackupModalProvider"); + return context; +}; diff --git a/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx b/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx index 4355df19..c77f2730 100644 --- a/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx @@ -6,7 +6,7 @@ import {MoreHorizontal, Trash2} from "lucide-react"; import {FilterItem, FiltersDropdown} from "@/components/wrappers/common/table/filters"; import {DataTable} from "@/components/wrappers/common/table/data-table"; import {useMemo, useState} from "react"; -import {Backup, DatabaseWith} from "@/db/schema/07_database"; +import {Backup, BackupWith, DatabaseWith} from "@/db/schema/07_database"; import {Setting} from "@/db/schema/01_setting"; import {useMutation} from "@tanstack/react-query"; import {deleteBackupAction} from "@/features/dashboard/restore/restore.action"; @@ -19,7 +19,7 @@ type DatabaseBackupListProps = { isAlreadyRestore: boolean; settings: Setting; database: DatabaseWith; - backups: Backup[]; + backups: BackupWith[]; activeMember: MemberWithUser } diff --git a/src/components/wrappers/dashboard/projects/database/database-content.tsx b/src/components/wrappers/dashboard/projects/database/database-content.tsx new file mode 100644 index 00000000..6f76298b --- /dev/null +++ b/src/components/wrappers/dashboard/projects/database/database-content.tsx @@ -0,0 +1,39 @@ +"use client" +import {DatabaseBackupActionsModal} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-modal"; +import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs"; +import {Setting} from "@/db/schema/01_setting"; +import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database"; +import {MemberWithUser} from "@/db/schema/03_organization"; +import {useBackupModal} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; + + +export type DatabaseContentProps = { + settings: Setting, + backups: BackupWith[], + restorations: Restoration[], + isAlreadyRestore: boolean, + database: DatabaseWith, + activeMember: MemberWithUser +} + + +export const DatabaseContent = ({ + settings, + backups, + activeMember, + isAlreadyRestore, + restorations, + database + }: DatabaseContentProps) => { + const {} = useBackupModal(); + + return ( + <> + + + + ) +} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx index 39848f0e..7223723c 100644 --- a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx @@ -4,7 +4,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; import {useEffect, useState} from "react"; import {useRouter, useSearchParams} from "next/navigation"; import {eventUpdate} from "@/types/events"; -import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_database"; +import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database"; import {Setting} from "@/db/schema/01_setting"; import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list"; import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list"; @@ -12,7 +12,7 @@ import {MemberWithUser} from "@/db/schema/03_organization"; export type DatabaseTabsProps = { settings: Setting, - backups: Backup[], + backups: BackupWith[], restorations: Restoration[], isAlreadyRestore: boolean, database: DatabaseWith, @@ -39,6 +39,7 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => { }; }, []); + useEffect(() => { const newTab = searchParams.get("tab") ?? "backup"; setTab(newTab); diff --git a/src/db/schema/07_database.ts b/src/db/schema/07_database.ts index 59bf1c39..4e14e1dd 100644 --- a/src/db/schema/07_database.ts +++ b/src/db/schema/07_database.ts @@ -8,7 +8,7 @@ import {z} from "zod"; import {timestamps} from "@/db/schema/00_common"; import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy"; import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy"; -import {backupStorage} from "@/db/schema/14_storage-backup"; +import {BackupStorage, backupStorage} from "@/db/schema/14_storage-backup"; export const database = pgTable("databases", { id: uuid("id").primaryKey().defaultRandom(), @@ -126,3 +126,10 @@ export type DatabaseWith = Database & { storagePolicies?: StoragePolicy[] | null; }; + +export type BackupWith = Backup & { + restorations?: Restoration[] | null; + storages?: BackupStorage[] | null; +}; + + diff --git a/src/db/schema/14_storage-backup.ts b/src/db/schema/14_storage-backup.ts index 3444c87c..e7dbfb22 100644 --- a/src/db/schema/14_storage-backup.ts +++ b/src/db/schema/14_storage-backup.ts @@ -1,8 +1,10 @@ import { pgTable, uuid, text, integer, pgEnum } from "drizzle-orm/pg-core"; import { timestamps } from "@/db/schema/00_common"; -import { storageChannel } from "@/db/schema/12_storage-channel"; -import {backup} from "@/db/schema/07_database"; +import {StorageChannel, storageChannel} from "@/db/schema/12_storage-channel"; +import {Backup, backup, Restoration} from "@/db/schema/07_database"; import {relations} from "drizzle-orm"; +import {createSelectSchema} from "drizzle-zod"; +import {z} from "zod"; export const backupStorageStatusEnum = pgEnum("backup_storage_status", [ "pending", @@ -35,4 +37,14 @@ export const backupStorageRelations = relations(backupStorage, ({ one }) => ({ fields: [backupStorage.storageChannelId], references: [storageChannel.id], }), -})); \ No newline at end of file +})); + + +export const backupStorageSchema = createSelectSchema(backupStorage); +export type BackupStorage = z.infer; + +export type BackupStorageWith = BackupStorage & { + storageChannel?: StorageChannel | null; +}; + + diff --git a/src/db/services/backup.ts b/src/db/services/backup.ts new file mode 100644 index 00000000..d9234be4 --- /dev/null +++ b/src/db/services/backup.ts @@ -0,0 +1,19 @@ +"use server" +import {eq} from "drizzle-orm"; +import * as drizzleDb from "@/db"; +import {db} from "@/db"; + +export async function getDatabaseBackups(databaseId: string) { + return await db.query.backup.findMany({ + where: eq(drizzleDb.schemas.backup.databaseId, databaseId), + with: { + restorations: true, + storages: { + with: { + storageChannel: true, + }, + }, + }, + orderBy: (b, {desc}) => [desc(b.createdAt)], + }); +} diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx index d8310ca3..36e0ccd2 100644 --- a/src/features/dashboard/backup/columns.tsx +++ b/src/features/dashboard/backup/columns.tsx @@ -32,6 +32,7 @@ import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/compon import {MemberWithUser} from "@/db/schema/03_organization"; import {formatLocalizedDate} from "@/utils/date-formatting"; import {formatBytes, isImportedFilename} from "@/utils/text"; +import {DatabaseActionsCell} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-cell"; export function backupColumns( @@ -92,6 +93,10 @@ export function backupColumns( return ; }, }, + { + id: "actions2", + cell: ({row}) => , + }, { id: "actions", cell: ({row, table}) => { diff --git a/src/features/storages/helpers.ts b/src/features/storages/helpers.ts index 8479de79..e52d632a 100644 --- a/src/features/storages/helpers.ts +++ b/src/features/storages/helpers.ts @@ -1,11 +1,11 @@ -import { Backup, DatabaseWith } from "@/db/schema/07_database"; -import { dispatchStorage } from "@/features/storages/dispatch"; -import type { StorageInput, StorageResult } from "@/features/storages/types"; +import {Backup, DatabaseWith} from "@/db/schema/07_database"; +import {dispatchStorage} from "@/features/storages/dispatch"; +import type {StorageInput, StorageResult} from "@/features/storages/types"; import * as drizzleDb from "@/db"; -import { withUpdatedAt } from "@/db/utils"; -import { eq } from "drizzle-orm"; -import { db } from "@/db"; -import { createHash } from "crypto"; +import {withUpdatedAt} from "@/db/utils"; +import {eq} from "drizzle-orm"; +import {db} from "@/db"; +import {createHash} from "crypto"; function computeChecksum(buffer: Buffer): string { return createHash("sha256").update(buffer).digest("hex"); @@ -20,7 +20,7 @@ export async function storeBackupFiles( const settings = await db.query.setting.findFirst({ where: eq(drizzleDb.schemas.setting.name, "system"), - with: { storageChannel: true }, + with: {storageChannel: true}, }); const defaultPolicy = settings?.storageChannel @@ -62,16 +62,16 @@ export async function storeBackupFiles( const input: StorageInput = { action: "upload", - data: { path, file }, + data: {path, file}, }; // const result = await dispatchStorage(input, policy.id); let result: StorageResult; try { - if (policy.id){ + if (policy.id) { result = await dispatchStorage(input, policy.id); - }else{ + } else { result = await dispatchStorage(input, undefined, policy.storageChannelId); } @@ -86,7 +86,7 @@ export async function storeBackupFiles( await db .update(drizzleDb.schemas.backupStorage) - .set(withUpdatedAt({ status: result.success ? "success" : "failed" })) + .set(withUpdatedAt({status: result.success ? "success" : "failed"})) .where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id)); return result; @@ -99,7 +99,10 @@ export async function storeBackupFiles( await db .update(drizzleDb.schemas.backup) - .set(withUpdatedAt({ status: backupStatus })) + .set(withUpdatedAt({ + status: backupStatus, + fileSize: size + })) .where(eq(drizzleDb.schemas.backup.id, backup.id)); return results; diff --git a/src/features/storages/providers/local.ts b/src/features/storages/providers/local.ts index 05f466bd..6114597b 100644 --- a/src/features/storages/providers/local.ts +++ b/src/features/storages/providers/local.ts @@ -16,7 +16,7 @@ export async function uploadLocal( const dir = path.dirname(fullPath); - await mkdir(dir, { recursive: true }); + await mkdir(dir, {recursive: true}); await writeFile(fullPath, input.data.file); return { @@ -31,14 +31,14 @@ export async function getLocal( input: { data: StorageGetInput } ): Promise { const base = config.baseDir || BASE_DIR; - const filePath = path.join(base, input.data.path) + const filePath = path.join(process.cwd(), base, input.data.path) const fileName = path.basename(input.data.path); - const file = await readFile(filePath); + const file = await readFile(filePath); if (!fs.existsSync(filePath)) { console.error("File not found at:", filePath); - return({ + return ({ success: false, provider: 'local', });