From 5bd24574c3497dd2f6ec28d23ceea20d74600fe9 Mon Sep 17 00:00:00 2001 From: charles-gauthereau Date: Tue, 10 Dec 2024 11:22:36 +0100 Subject: [PATCH] Working on the hidde restore action if already a restauration is waiting. --- app/(customer)/dashboard/layout.tsx | 1 - .../database/[databaseId]/page.tsx | 63 ++++++---------- app/globals.css | 71 ------------------ .../wrappers/BackupButton/BackupButton.tsx | 6 +- .../Projects/Database/DatabaseKpi.tsx | 40 ++++++++++ .../Projects/Database/DatabaseTabs.tsx | 11 +-- .../wrappers/TooltipCustom/TooltipCustom.tsx | 34 +++++++++ .../table/data-table-with-pagination.tsx | 8 +- src/components/wrappers/table/data-table.tsx | 60 ++------------- src/features/backup/columns.tsx | 73 +++++++++++++++---- src/features/restore/restore.action.ts | 48 ++++++++++++ src/utils/date-formatting.ts | 1 + 12 files changed, 221 insertions(+), 195 deletions(-) create mode 100644 src/components/wrappers/Dashboard/Projects/Database/DatabaseKpi.tsx create mode 100644 src/components/wrappers/TooltipCustom/TooltipCustom.tsx create mode 100644 src/features/restore/restore.action.ts diff --git a/app/(customer)/dashboard/layout.tsx b/app/(customer)/dashboard/layout.tsx index 13b0f9e6..05a98994 100644 --- a/app/(customer)/dashboard/layout.tsx +++ b/app/(customer)/dashboard/layout.tsx @@ -21,7 +21,6 @@ export default async function Layout({children}: { children: React.ReactNode }) } if (!user) redirect('/login') - return (
diff --git a/app/(customer)/dashboard/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/projects/[projectId]/database/[databaseId]/page.tsx index c338f116..0f93bb5e 100644 --- a/app/(customer)/dashboard/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/projects/[projectId]/database/[databaseId]/page.tsx @@ -3,55 +3,54 @@ import {prisma} from "@/prisma"; import {notFound} from "next/navigation"; import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; import {BackupButton} from "@/components/wrappers/BackupButton/BackupButton"; -import {Card, CardContent, CardHeader} from "@/components/ui/card"; import {DatabaseTabs} from "@/components/wrappers/Dashboard/Projects/Database/DatabaseTabs"; -import {formatDateLastContact} from "@/utils/date-formatting"; +import {DatabaseKpi} from "@/components/wrappers/Dashboard/Projects/Database/DatabaseKpi"; export default async function RoutePage(props: PageParams<{ databaseId: string }>) { const {databaseId} = await props.params - const database = await prisma.database.findUnique({ where: { id: databaseId, }, }) - console.log("database", database) - if (!database) { notFound() } + const backups = await prisma.backup.findMany({ where: { databaseId: database.id + }, + include:{ + restaurations: {} } }) - console.log(backups) + const restaurations = await prisma.restauration.findMany({ + where: { + databaseId: databaseId, + } + }) + + + const isAlreadyBackup = !!backups.find(backup => backup.status === "waiting"); + const isAlreadyRestore = !!restaurations.find(restoration => restoration.status === "waiting"); const totalBackups = await prisma.backup.count({ where: { databaseId: databaseId, }, }); - const successfulBackups = await prisma.backup.count({ where: { databaseId: databaseId, status: 'success', }, }); - const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null - // const restaurations = await prisma.restauration.findMany({ - // where: { - // databaseId: databaseId, - // } - // }) - - return (
@@ -59,39 +58,19 @@ export default async function RoutePage(props: PageParams<{ databaseId: string } {database.name} - +
{database.description} -
- - - Backups - - - - - - Success rate - - - {successRate ?? "Unavailable for now."} - - - - - Last contact - - - {formatDateLastContact(database.lastContact)} - - -
- +
diff --git a/app/globals.css b/app/globals.css index 1a07323f..08f0c206 100644 --- a/app/globals.css +++ b/app/globals.css @@ -16,77 +16,6 @@ } } -/*@layer base {*/ -/* :root {*/ -/* --background: 0 0% 100%;*/ -/* --foreground: 0 0% 3.9%;*/ -/* --card: 0 0% 100%;*/ -/* --card-foreground: 0 0% 3.9%;*/ -/* --popover: 0 0% 100%;*/ -/* --popover-foreground: 0 0% 3.9%;*/ -/* --primary: 0 0% 9%;*/ -/* --primary-foreground: 0 0% 98%;*/ -/* --secondary: 0 0% 96.1%;*/ -/* --secondary-foreground: 0 0% 9%;*/ -/* --muted: 0 0% 96.1%;*/ -/* --muted-foreground: 0 0% 45.1%;*/ -/* --accent: 0 0% 96.1%;*/ -/* --accent-foreground: 0 0% 9%;*/ -/* --destructive: 0 84.2% 60.2%;*/ -/* --destructive-foreground: 0 0% 98%;*/ -/* --border: 0 0% 89.8%;*/ -/* --input: 0 0% 89.8%;*/ -/* --ring: 0 0% 3.9%;*/ -/* --chart-1: 12 76% 61%;*/ -/* --chart-2: 173 58% 39%;*/ -/* --chart-3: 197 37% 24%;*/ -/* --chart-4: 43 74% 66%;*/ -/* --chart-5: 27 87% 67%;*/ -/* --radius: 0.5rem;*/ -/* --sidebar-background: 0 0% 98%;*/ -/* --sidebar-foreground: 240 5.3% 26.1%;*/ -/* --sidebar-primary: 240 5.9% 10%;*/ -/* --sidebar-primary-foreground: 0 0% 98%;*/ -/* --sidebar-accent: 240 4.8% 95.9%;*/ -/* --sidebar-accent-foreground: 240 5.9% 10%;*/ -/* --sidebar-border: 220 13% 91%;*/ -/* --sidebar-ring: 217.2 91.2% 59.8%;*/ -/* }*/ -/* .dark {*/ -/* --background: 0 0% 3.9%;*/ -/* --foreground: 0 0% 98%;*/ -/* --card: 0 0% 3.9%;*/ -/* --card-foreground: 0 0% 98%;*/ -/* --popover: 0 0% 3.9%;*/ -/* --popover-foreground: 0 0% 98%;*/ -/* --primary: 0 0% 98%;*/ -/* --primary-foreground: 0 0% 9%;*/ -/* --secondary: 0 0% 14.9%;*/ -/* --secondary-foreground: 0 0% 98%;*/ -/* --muted: 0 0% 14.9%;*/ -/* --muted-foreground: 0 0% 63.9%;*/ -/* --accent: 0 0% 14.9%;*/ -/* --accent-foreground: 0 0% 98%;*/ -/* --destructive: 0 62.8% 30.6%;*/ -/* --destructive-foreground: 0 0% 98%;*/ -/* --border: 0 0% 14.9%;*/ -/* --input: 0 0% 14.9%;*/ -/* --ring: 0 0% 83.1%;*/ -/* --chart-1: 220 70% 50%;*/ -/* --chart-2: 160 60% 45%;*/ -/* --chart-3: 30 80% 55%;*/ -/* --chart-4: 280 65% 60%;*/ -/* --chart-5: 340 75% 55%;*/ -/* --sidebar-background: 240 5.9% 10%;*/ -/* --sidebar-foreground: 240 4.8% 95.9%;*/ -/* --sidebar-primary: 224.3 76.3% 48%;*/ -/* --sidebar-primary-foreground: 0 0% 100%;*/ -/* --sidebar-accent: 240 3.7% 15.9%;*/ -/* --sidebar-accent-foreground: 240 4.8% 95.9%;*/ -/* --sidebar-border: 240 3.7% 15.9%;*/ -/* --sidebar-ring: 217.2 91.2% 59.8%;*/ -/* }*/ -/*}*/ @layer base { diff --git a/src/components/wrappers/BackupButton/BackupButton.tsx b/src/components/wrappers/BackupButton/BackupButton.tsx index d8e06238..c5ebe063 100644 --- a/src/components/wrappers/BackupButton/BackupButton.tsx +++ b/src/components/wrappers/BackupButton/BackupButton.tsx @@ -2,16 +2,14 @@ import {useMutation} from "@tanstack/react-query"; import {backupButtonAction} from "@/components/wrappers/BackupButton/backup-button.action"; import {toast} from "sonner"; -import {Backup} from "@prisma/client"; import {useRouter} from "next/navigation"; -import {ActionResult} from "@/types/action-type"; import {ButtonWithLoading} from "@/components/wrappers/Button/ButtonWithLoading/ButtonWithLoading"; export type BackupButtonProps = { databaseId: string; + disable: boolean } - export const BackupButton = (props: BackupButtonProps) => { const router = useRouter(); const mutation = useMutation({ @@ -30,9 +28,9 @@ export const BackupButton = (props: BackupButtonProps) => { await mutation.mutateAsync(props.databaseId) } - return ( { + return ( +
+ + + Backups + + + + + + Success rate + + + {props.successRate ?? "Unavailable for now."} + + + + + Last contact + + + {formatDateLastContact(props.database.lastContact)} + + +
+ ) +} \ No newline at end of file diff --git a/src/components/wrappers/Dashboard/Projects/Database/DatabaseTabs.tsx b/src/components/wrappers/Dashboard/Projects/Database/DatabaseTabs.tsx index b5dfaa7f..a3664afc 100644 --- a/src/components/wrappers/Dashboard/Projects/Database/DatabaseTabs.tsx +++ b/src/components/wrappers/Dashboard/Projects/Database/DatabaseTabs.tsx @@ -6,7 +6,8 @@ import {Backup, Restauration} from "@prisma/client"; export type DatabaseTabsProps = { backups: Backup[] - // restaurations: Restauration[] + restaurations: Restauration[] + isAlreadyRestore: boolean } export const DatabaseTabs = (props: DatabaseTabsProps) => { @@ -19,12 +20,12 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => { - + - {/**/} - {/* */} - {/**/} + + + ) } \ No newline at end of file diff --git a/src/components/wrappers/TooltipCustom/TooltipCustom.tsx b/src/components/wrappers/TooltipCustom/TooltipCustom.tsx new file mode 100644 index 00000000..860fd1cb --- /dev/null +++ b/src/components/wrappers/TooltipCustom/TooltipCustom.tsx @@ -0,0 +1,34 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import {PropsWithChildren} from "react"; + +export type TooltipCustomProps = PropsWithChildren<{ + text: string + disabled?: any +}>; + +export function TooltipCustom(props: TooltipCustomProps) { + return ( + <> + {props.disabled ? + + + + {props.children} + + +

{props.text}

+
+
+
+ : + props.children + } + + + ) +} diff --git a/src/components/wrappers/table/data-table-with-pagination.tsx b/src/components/wrappers/table/data-table-with-pagination.tsx index a8ae4e2e..7e693805 100644 --- a/src/components/wrappers/table/data-table-with-pagination.tsx +++ b/src/components/wrappers/table/data-table-with-pagination.tsx @@ -11,9 +11,10 @@ import {TablePagination} from "@/components/wrappers/table/table-pagination"; interface DataTableProps { columns: ColumnDef[] data: TData[] + extendedProps?: any } -export function DataTableWithPagination({columns, data}: DataTableProps) { +export function DataTableWithPagination({columns, data, extendedProps}: DataTableProps) { const [sorting, setSorting] = useState([]) @@ -27,11 +28,14 @@ export function DataTableWithPagination({columns, data}: DataTabl state: { sorting, }, + meta: { + extendedProps: extendedProps, + } }) return (
- +
) diff --git a/src/components/wrappers/table/data-table.tsx b/src/components/wrappers/table/data-table.tsx index feec740d..2ff0cf5c 100644 --- a/src/components/wrappers/table/data-table.tsx +++ b/src/components/wrappers/table/data-table.tsx @@ -2,7 +2,7 @@ import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/c import {flexRender} from "@tanstack/react-table"; export type dataTableProps = { - table: any + table: any, } export const DataTable = ({table}: dataTableProps) => { @@ -10,7 +10,6 @@ export const DataTable = ({table}: dataTableProps) => { return (
- {table.getHeaderGroups().map((headerGroup) => ( @@ -38,7 +37,10 @@ export const DataTable = ({table}: dataTableProps) => { > {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} ))} @@ -55,55 +57,3 @@ export const DataTable = ({table}: dataTableProps) => { ) } -// import { useMemo } from "react"; -// import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -// import { flexRender } from "@tanstack/react-table"; -// -// export type dataTableProps = { -// table: any; -// }; -// -// export const DataTable = ({ table }: dataTableProps) => { -// // Memoize the header and rows to prevent unnecessary re-renders -// const headerGroups = useMemo(() => table.getHeaderGroups(), [table]); -// const rows = useMemo(() => table.getRowModel().rows, [table]); -// -// return ( -//
-//
-// -// {headerGroups.map((headerGroup) => ( -// -// {headerGroup.headers.map((header) => ( -// -// {header.isPlaceholder -// ? null -// : flexRender(header.column.columnDef.header, header.getContext())} -// -// ))} -// -// ))} -// -// -// {rows?.length ? ( -// rows.map((row) => ( -// -// {row.getVisibleCells().map((cell) => ( -// -// {flexRender(cell.column.columnDef.cell, cell.getContext())} -// -// ))} -// -// )) -// ) : ( -// -// -// No results. -// -// -// )} -// -//
-//
-// ); -// }; diff --git a/src/features/backup/columns.tsx b/src/features/backup/columns.tsx index 0a645e8b..da1a4e36 100644 --- a/src/features/backup/columns.tsx +++ b/src/features/backup/columns.tsx @@ -13,7 +13,12 @@ import {Button} from "@/components/ui/button"; import {Download, MoreHorizontal, Trash2} from "lucide-react"; import {ReloadIcon} from "@radix-ui/react-icons"; import {Backup} from "@prisma/client"; - +import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action"; +import {useMutation} from "@tanstack/react-query"; +import {createRestaurationAction} from "@/features/restore/restore.action"; +import {toast} from "sonner"; +import {useRouter} from "next/navigation"; +import {TooltipCustom} from "@/components/wrappers/TooltipCustom/TooltipCustom"; export const backupColumns: ColumnDef[] = [ @@ -37,7 +42,41 @@ export const backupColumns: ColumnDef[] = [ }, { id: "actions", - cell: ({row}) => { + cell: ({row, table}) => { + const status = row.getValue("status") + const rowData: Backup = row.original + const fileName = rowData.file + // @ts-ignore + const {extendedProps} = table.options.meta; + + + const isRestauring = !!rowData.restaurations.find(restauration => restauration.status === "waiting"); + + const router = useRouter() + + const handleDownload = async (fileName: string) => { + const url = await getFileUrlPresignedLocal(fileName) + window.open(url, '_self'); + } + + + const mutationRestore = useMutation({ + mutationFn: async () => { + const restauration = await createRestaurationAction({ + backupId: rowData.id, + databaseId: rowData.databaseId + }) + if (restauration.data.success) { + toast.success(restauration.data.actionSuccess?.message || "Restauration created successfully!"); + router.refresh() + } else { + toast.error(restauration.serverError || "Failed to create restauration."); + } + }}) + + const handleRestore = async () => { + await mutationRestore.mutateAsync() + } return ( @@ -49,24 +88,28 @@ export const backupColumns: ColumnDef[] = [ Actions - - { - }}> - Restore - - - { - }}> - Download - - + {status == "success" ? + <> + + { + await handleRestore() + }}> + Restore + + + { + await handleDownload(fileName) + }}> + Download + + + : null} - { }}> Delete - ) diff --git a/src/features/restore/restore.action.ts b/src/features/restore/restore.action.ts new file mode 100644 index 00000000..d4011a58 --- /dev/null +++ b/src/features/restore/restore.action.ts @@ -0,0 +1,48 @@ +"use server" +import {userAction} from "@/safe-actions"; +import {z} from "zod"; +import {prisma} from "@/prisma"; +import {ServerActionResult} from "@/types/action-type"; +import {Restauration} from "@prisma/client"; + + +export const createRestaurationAction = userAction + .schema(z.object({ + backupId: z.string(), + databaseId: z.string(), + })) + .action(async ({parsedInput, ctx}): Promise> => { + + try { + const restauration = await prisma.restauration.create({ + data: { + databaseId: parsedInput.databaseId, + backupId: parsedInput.backupId, + status: "waiting", + }, + }); + + return { + success: true, + value: restauration, + actionSuccess: { + message: "Restauration has been successfully created.", + messageParams: { restaurationId: restauration.id }, + }, + }; + } catch (error) { + console.error("Error creating restauration:", error); + return { + success: false, + actionError: { + message: "Failed to create backup.", + status: 500, + cause: error instanceof Error ? error.message : "Unknown error", + messageParams: { message: "Error creating the restauration" }, + }, + }; + } + + + + }); diff --git a/src/utils/date-formatting.ts b/src/utils/date-formatting.ts index a9184eec..9ef19cd2 100644 --- a/src/utils/date-formatting.ts +++ b/src/utils/date-formatting.ts @@ -10,6 +10,7 @@ export function timeAgo(rawDate: string | number | Date) { } export function formatDateLastContact(lastContact: string | number | Date | null) { + console.log(lastContact) return lastContact ? format(new Date(lastContact), 'dd/MM/yyyy HH:mm') : "Never connected."