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 3cc4888a..fefe7c74 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -1,21 +1,13 @@ 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 {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi"; -import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button"; +import {Page} from "@/features/layout/page"; import {db} from "@/db"; import {eq, and, inArray} from "drizzle-orm"; import * as drizzleDb from "@/db"; import {getOrganizationProjectDatabases} from "@/lib/services"; import {getActiveMember, getOrganization} from "@/lib/auth/auth"; -import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; -import {capitalizeFirstLetter} from "@/utils/text"; import {getOrganizationChannels} from "@/db/services/notification-channel"; -import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal"; 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"; @@ -106,60 +98,22 @@ export default async function RoutePage(props: PageParams<{ return ( - -
- -
- {capitalizeFirstLetter(dbItem.name)} -
- {!isMember && ( -
-
- - - } - channels={activeOrganizationChannels} - organizationId={organization.id} - /> - } - kind={"storage"} - channels={activeOrganizationStorageChannels} - organizationId={organization.id} - /> - -
-
- -
-
- )} -
- - -
- - {dbItem.description && ( - {dbItem.description} - )} - - - - - - + + +
); -} +} \ No newline at end of file diff --git a/app/api/agent/[agentId]/backup/route.ts b/app/api/agent/[agentId]/backup/route.ts index ca978a82..93917dd0 100644 --- a/app/api/agent/[agentId]/backup/route.ts +++ b/app/api/agent/[agentId]/backup/route.ts @@ -1,7 +1,6 @@ import {NextResponse} from "next/server"; import {isUuidv4} from "@/utils/verify-uuid"; import {v4 as uuidv4} from "uuid"; -import {eventEmitter} from "../../../events/route"; import * as drizzleDb from "@/db"; import {db} from "@/db"; import {Backup} from "@/db/schema/07_database"; @@ -25,7 +24,6 @@ export async function POST( ); } - eventEmitter.emit('modification', {update: true}); const agentId = (await params).agentId; const formData = await request.formData(); @@ -132,11 +130,9 @@ export async function POST( const buffer = Buffer.from(await decryptedFile.arrayBuffer()); - console.log(extension) const storageResults = await storeBackupFiles(backup, database, buffer, fileName) - eventEmitter.emit('modification', {update: true}); await sendNotificationsBackupRestore(database, "success_backup"); @@ -153,7 +149,6 @@ export async function POST( .set(withUpdatedAt({status: 'failed'})) .where(eq(drizzleDb.schemas.backup.id, backup.id)); - eventEmitter.emit('modification', {update: true}); await sendNotificationsBackupRestore(database, "error_backup"); diff --git a/app/api/agent/[agentId]/restore/route.ts b/app/api/agent/[agentId]/restore/route.ts index d4bd7ecb..827a074b 100644 --- a/app/api/agent/[agentId]/restore/route.ts +++ b/app/api/agent/[agentId]/restore/route.ts @@ -1,6 +1,5 @@ import {NextResponse} from "next/server"; import {isUuidv4} from "@/utils/verify-uuid"; -import {eventEmitter} from "../../../events/route"; import * as drizzleDb from "@/db"; import {db} from "@/db"; import {and, eq} from "drizzle-orm"; @@ -19,7 +18,6 @@ export async function POST( ) { try { - eventEmitter.emit('modification', {update: true}); const agentId = (await params).agentId const body: BodyResultRestore = await request.json(); @@ -72,7 +70,6 @@ export async function POST( details: "Restoration successfully updated" } - eventEmitter.emit('modification', {update: true}); return Response.json(response, {status: 200}) } catch (error) { diff --git a/app/api/agent/[agentId]/status/route.ts b/app/api/agent/[agentId]/status/route.ts index a0749a0b..1f0e47cf 100644 --- a/app/api/agent/[agentId]/status/route.ts +++ b/app/api/agent/[agentId]/status/route.ts @@ -1,6 +1,5 @@ import {NextResponse} from "next/server"; import {handleDatabases} from "./helpers"; -import {eventEmitter} from "../../../events/route"; import * as drizzleDb from "@/db"; import {db} from "@/db"; import {EDbmsSchema} from "@/db/schema/types"; @@ -56,7 +55,6 @@ export async function POST( })) .where(eq(drizzleDb.schemas.agent.id, agentId)); - eventEmitter.emit('modification', {update: true}); const response = { agent: { diff --git a/app/api/events/route.ts b/app/api/events/route.ts deleted file mode 100644 index a24108ea..00000000 --- a/app/api/events/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {EventEmitter} from 'events'; -import {auth} from "@/lib/auth/auth"; -import {headers} from "next/headers"; -import {NextResponse} from "next/server"; - -export const eventEmitter = new EventEmitter(); - -export async function GET(request: Request) { - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session) { - return NextResponse.json({error: "Unauthorized"}, {status: 403}); - } - - return new Response( - new ReadableStream({ - start(controller) { - console.log('Stream started'); - const handleModification = (data: any) => { - console.log('Modification event triggered:', data); - controller.enqueue(`event: modification\n`); - controller.enqueue(`data: ${JSON.stringify(data)}\n\n`); - }; - - eventEmitter.on('modification', handleModification); - - // Handle client disconnect - request.signal.addEventListener('abort', () => { - console.log('Client disconnected'); - controller.close(); - eventEmitter.off('modification', handleModification); - }); - }, - }), - { - status: 200, - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }, - } - ); -} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index e8b4b397..c4870e80 100644 --- a/app/globals.css +++ b/app/globals.css @@ -71,7 +71,7 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(0.611 0.204 42.149); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); @@ -82,7 +82,7 @@ --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --ring: oklch(0.611 0.204 42.149); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); @@ -90,12 +90,12 @@ --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary: oklch(0.611 0.204 42.149); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-ring: oklch(0.611 0.204 42.149); } .dark { @@ -105,8 +105,8 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); + --primary: oklch(0.704 0.191 47.132); + --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); @@ -116,7 +116,7 @@ --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); + --ring: oklch(0.704 0.191 47.132); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); @@ -124,12 +124,12 @@ --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary: oklch(0.704 0.191 47.132); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --sidebar-ring: oklch(0.704 0.191 47.132); } @layer base { diff --git a/src/components/wrappers/common/button/button-with-confirm.tsx b/src/components/wrappers/common/button/button-with-confirm.tsx index e4472e09..5a820042 100644 --- a/src/components/wrappers/common/button/button-with-confirm.tsx +++ b/src/components/wrappers/common/button/button-with-confirm.tsx @@ -1,15 +1,15 @@ "use client" import {Button, ButtonVariantsProps} from "@/components/ui/button"; -import {useState} from "react"; +import {ReactNode, useState} from "react"; import {Loader2} from "lucide-react"; import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; import {cn} from "@/lib/utils"; -import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip"; +import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; export type ButtonWithConfirmProps = { title: string; description: string; - button: { + button?: { main: { className?: string; type?: "button" | "submit" | "reset" | undefined; @@ -37,6 +37,11 @@ export type ButtonWithConfirmProps = { onClick?: () => void; }; }; + children?: ReactNode; + onConfirm?: (e: React.MouseEvent) => void; + onCancel?: (e: React.MouseEvent) => void; + confirmButtonText?: string; + cancelButtonText?: string; isPending?: boolean; }; @@ -44,41 +49,86 @@ export type ButtonWithConfirmProps = { export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => { const [isConfirming, setIsConfirming] = useState(false); - return ( - + const isLegacy = !!props.button; + const isDisabled = isLegacy ? !!props.button?.main.disabled : false; + + const handleConfirm = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (isLegacy) { + props.button?.confirm.onClick?.(); + } else { + props.onConfirm?.(e); + } + setIsConfirming(false); + }; + + const handleCancel = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (isLegacy) { + props.button?.cancel.onClick?.(); + } else { + props.onCancel?.(e); + } + setIsConfirming(false); + }; + + const triggerContent = isLegacy ? ( + + ) : ( +
{ + e.preventDefault(); + e.stopPropagation(); + setIsConfirming(true); + }}> + {props.children} +
+ ); + + const withTooltip = isLegacy && props.button?.main.tooltipText && isDisabled ? ( + - - - - - +
{triggerContent}
- {props.button.main.tooltipText && props.button.main.disabled && ( - -

{props.button.main.tooltipText}

-
- )} + +

{props.button?.main.tooltipText}

+
- +
+ ) : triggerContent; + + return ( + + + {withTooltip} + + e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + onClick={(e) => e.stopPropagation()} + onMouseMove={(e) => e.stopPropagation()} + onMouseEnter={(e) => e.stopPropagation()} + onMouseLeave={(e) => e.stopPropagation()} + onOpenAutoFocus={(e) => e.preventDefault()} + >

{props.title}

@@ -86,34 +136,26 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
); -}; - +}; \ No newline at end of file diff --git a/src/components/wrappers/common/button/button-with-loading.tsx b/src/components/wrappers/common/button/button-with-loading.tsx index 65fa244e..0773c7cd 100644 --- a/src/components/wrappers/common/button/button-with-loading.tsx +++ b/src/components/wrappers/common/button/button-with-loading.tsx @@ -83,7 +83,7 @@ export type ButtonWithLoadingProps = { icon?: ReactNode; variant?: keyof VariantButton; className?: string; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; isPending?: boolean; size?: keyof SizeButton; } & ButtonHTMLAttributes; @@ -100,7 +100,7 @@ export const ButtonWithLoading = ({ }: ButtonWithLoadingProps) => { return ( - -
-
-                    {props.code}
+        
+ {props.title && ( +
+ {props.title} +
+ )} +
+
+                    {props.code}
                 
+
); diff --git a/src/components/wrappers/common/dialog.tsx b/src/components/wrappers/common/dialog.tsx index 13b28e4a..c9f2cf5a 100644 --- a/src/components/wrappers/common/dialog.tsx +++ b/src/components/wrappers/common/dialog.tsx @@ -23,7 +23,7 @@ export const AgentRegistrationDialog = (props: agentRegistrationDialogProps) =>
diff --git a/src/components/wrappers/common/table/data-table.tsx b/src/components/wrappers/common/table/data-table.tsx index ad83010f..61e1bcc0 100644 --- a/src/components/wrappers/common/table/data-table.tsx +++ b/src/components/wrappers/common/table/data-table.tsx @@ -14,7 +14,7 @@ import { import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"; import {Input} from "@/components/ui/input"; -import {ReactNode, useEffect, useState} from "react"; +import {ReactNode, useMemo, useState} from "react"; import {TablePagination} from "./table-pagination"; import {Checkbox} from "@/components/ui/checkbox"; import {Button} from "@/components/ui/button"; @@ -63,35 +63,38 @@ export function DataTable({ const [columnFilters, setColumnFilters] = useState([]); const [rowSelection, setRowSelection] = useState({}); - useEffect(() => { - setRowSelection({}); - }, [data]); - - if (enableSelect && data.length > 0) { + const finalColumns = useMemo(() => { const selectColumnExists = columns.some((column) => column.id === "select"); - - if (!selectColumnExists) { - columns.unshift({ - id: "select", - header: ({table}) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({row}) => row.toggleSelected(!!value)} - aria-label="Select row"/>, - enableSorting: false, - enableHiding: false, - }); + if (enableSelect && data.length > 0 && !selectColumnExists) { + return [ + { + id: "select", + header: ({table}: {table: any}) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({row}: {row: any}) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + ...columns, + ]; } - } + return columns; + }, [columns, enableSelect, data.length]); const table = useReactTable({ data, - columns, + columns: finalColumns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, @@ -99,6 +102,10 @@ export function DataTable({ onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onRowSelectionChange: setRowSelection, + getRowId: (row: any) => row.id || row.uuid, + autoResetPageIndex: false, + autoResetExpanded: false, + enableRowSelection: true, state: { sorting, columnFilters, diff --git a/src/components/wrappers/dashboard/agent/agent-card-key/agent-card-key.tsx b/src/components/wrappers/dashboard/agent/agent-card-key/agent-card-key.tsx index 8d4eed19..8936d7ab 100644 --- a/src/components/wrappers/dashboard/agent/agent-card-key/agent-card-key.tsx +++ b/src/components/wrappers/dashboard/agent/agent-card-key/agent-card-key.tsx @@ -1,23 +1,88 @@ "use client"; -import {PasswordInput} from "@/components/ui/password-input"; + +import {Input} from "@/components/ui/input"; +import {Label} from "@/components/ui/label"; +import {Button} from "@/components/ui/button"; +import {Copy, Check} from "lucide-react"; import {useState} from "react"; -import {CopyButton} from "@/components/wrappers/common/button/copy-button"; +import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button"; export type AgentCardKeyProps = { edgeKey: string; + agentName: string; }; -export const AgentCardKey = ({edgeKey}: AgentCardKeyProps) => { - const [code, setCode] = useState(`${edgeKey}`); +export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => { + const [isCopiedKey, setIsCopiedKey] = useState(false); + const [isCopiedCommand, setIsCopiedCommand] = useState(false); + + const command = `portabase agent "${agentName}" --key ${edgeKey}`; + + const handleCopy = async (text: string, setter: (v: boolean) => void) => { + await copyToClipboardWithMeta(text); + setter(true); + setTimeout(() => setter(false), 2000); + }; + + const handleFocus = (event: React.FocusEvent) => { + event.target.select(); + }; + return ( - <> - { - setCode(edgeKey); - }} - /> - - +
+
+
+ +
+ + +
+

+ Use this key for manual configuration of your agent. +

+
+ +
+ +
+ + +
+

+ Run this command on your server to automatically register the agent. +

+
+
+
); }; diff --git a/src/components/wrappers/dashboard/agent/agent-content.tsx b/src/components/wrappers/dashboard/agent/agent-content.tsx index 3c6f6cc5..5ad5c655 100644 --- a/src/components/wrappers/dashboard/agent/agent-content.tsx +++ b/src/components/wrappers/dashboard/agent/agent-content.tsx @@ -4,76 +4,109 @@ import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; import {Server} from "lucide-react"; import {formatDateLastContact} from "@/utils/date-formatting"; import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key"; +import {AgentWithDatabases} from "@/db/schema/08_agent"; +import {useQuery} from "@tanstack/react-query"; +import {getAgentAction} from "@/features/agents/agents.action"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import {Separator} from "@/components/ui/separator"; +import {Badge} from "@/components/ui/badge"; import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {AgentDatabaseCard} from "@/components/wrappers/dashboard/agent/agent-database-card"; -import {AgentWithDatabases} from "@/db/schema/08_agent"; -import {eventUpdate} from "@/types/events"; -import {useAutoRefresh} from "@/hooks/use-auto-refresh"; type AgentContentPageProps = { edgeKey: string; agent: AgentWithDatabases - } -export const AgentContentPage = ({edgeKey, agent}: AgentContentPageProps) => { +export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPageProps) => { - useAutoRefresh({ - poll: { - enabled: true, - intervalMs: 5000, + const {data} = useQuery({ + queryKey: ["agent-data", initialAgent.id], + queryFn: async () => { + const result = await getAgentAction(initialAgent.id); + return result?.data; }, - sse: { - enabled: true, - url: "/api/events", - eventName: "modification", - shouldRefresh: (data) => { - const update = data as eventUpdate; - return Boolean(update?.update); - }, + initialData: { + data: initialAgent }, + staleTime: 0, + gcTime: 0, + refetchInterval: 5000, }); + const agent = data?.data ?? initialAgent; return ( - <> +
- + - Databases - + Databases + -
{agent.databases.length}
-

Databases linked to this agent

+
{agent.databases.length}
+

Linked resources

- + - Last contact - + Last contact + -
{formatDateLastContact(agent.lastContact)}
-

Last contact with agent

+
{formatDateLastContact(agent.lastContact)}
+

Status heartbeat

-
- - - Edge Key - - - - - - - +
+ + + +
+ Registration & Setup + {!agent.lastContact && ( + + Action Required + + )} +
+
+ + + +
+
+
+ +
+
+
+

Managed Databases

+

+ Resources currently connected to this agent. +

+
+
+ + +
+
) } \ No newline at end of file diff --git a/src/components/wrappers/dashboard/agent/agent-database-columns.tsx b/src/components/wrappers/dashboard/agent/agent-database-columns.tsx new file mode 100644 index 00000000..c0fd3341 --- /dev/null +++ b/src/components/wrappers/dashboard/agent/agent-database-columns.tsx @@ -0,0 +1,79 @@ +"use client" + +import {ColumnDef} from "@tanstack/react-table" +import {Database} from "@/db/schema/07_database" +import Image from "next/image" +import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator" +import {formatDateLastContact} from "@/utils/date-formatting" +import Link from "next/link" +import {Button} from "@/components/ui/button" +import {ChevronRight} from "lucide-react" + +export const agentDatabaseColumns: ColumnDef[] = [ + { + accessorKey: "dbms", + header: "Type", + cell: ({row}) => ( +
+ {row.original.dbms} +
+ ) + }, + { + accessorKey: "name", + header: "Name", + cell: ({row}) => ( +
+ {row.original.name} + + {row.original.agentDatabaseId} + +
+ ) + }, + { + accessorKey: "lastContact", + header: "Last Contact", + cell: ({row}) => ( +
+ + {formatDateLastContact(row.original.lastContact)} + +
+ ) + }, + { + id: "status", + header: "Status", + cell: ({row}) => ( +
+ +
+ ) + }, + { + id: "actions", + header: () => null, + cell: ({row}) => { + const href = row.original.projectId + ? `/dashboard/projects/${row.original.projectId}/database/${row.original.id}` + : "#"; + + return ( +
+ +
+ ) + } + } +] diff --git a/src/components/wrappers/dashboard/agent/agent-modal-key/agent-modal-key.tsx b/src/components/wrappers/dashboard/agent/agent-modal-key/agent-modal-key.tsx index 5ccbef18..7966ddd9 100644 --- a/src/components/wrappers/dashboard/agent/agent-modal-key/agent-modal-key.tsx +++ b/src/components/wrappers/dashboard/agent/agent-modal-key/agent-modal-key.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DialogClose } from "@/components/ui/dialog"; import { generateEdgeKey } from "@/utils/edge_key"; import { PropsWithChildren } from "react"; @@ -16,26 +16,29 @@ export type agentRegistrationDialogProps = PropsWithChildren<{ export function AgentModalKey(props: agentRegistrationDialogProps) { const edge_key = generateEdgeKey(getServerUrl(), props.agent.id); - const code = `EDGE_KEY = ${edge_key}`; + const command = `portabase agent "${props.agent.name}" --key ${edge_key}`; return ( {props.children} - Agent Edge Key + Agent Connection -
+
+
-
- - -
+ + +
diff --git a/src/components/wrappers/dashboard/backup/backup-button/backup-button.tsx b/src/components/wrappers/dashboard/backup/backup-button/backup-button.tsx index 4e401cd2..71b15133 100644 --- a/src/components/wrappers/dashboard/backup/backup-button/backup-button.tsx +++ b/src/components/wrappers/dashboard/backup/backup-button/backup-button.tsx @@ -1,8 +1,6 @@ "use client"; -import {useRouter} from "next/navigation"; - -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {toast} from "sonner"; import {backupButtonAction} from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action"; @@ -16,7 +14,7 @@ export type BackupButtonProps = { }; export const BackupButton = (props: BackupButtonProps) => { - const router = useRouter(); + const queryClient = useQueryClient(); const isMobile = useIsMobile() const mutation = useMutation({ @@ -24,7 +22,7 @@ export const BackupButton = (props: BackupButtonProps) => { const backup = await backupButtonAction(databaseId); if (backup?.data?.success) { toast.success(backup.data.actionSuccess?.message || "Backup created successfully!"); - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]}); } else { toast.error(backup?.serverError || "Failed to create backup."); } 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 index db3d42ac..6e6a9cae 100644 --- a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-cell.tsx +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-cell.tsx @@ -33,7 +33,7 @@ export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore}: Da
- 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 index 3f15c23f..37c29145 100644 --- a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx @@ -1,7 +1,7 @@ "use client" import {Backup, BackupWith, Restoration} from "@/db/schema/07_database"; -import React from "react"; import {Swiper, SwiperSlide} from "swiper/react"; +//@ts-ignore import "swiper/css"; import "swiper/css/pagination"; import {Pagination, Mousewheel} from "swiper/modules"; @@ -12,7 +12,7 @@ import { 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 {useMutation, useQueryClient} 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"; @@ -29,8 +29,7 @@ import {SafeActionResult} from "next-safe-action"; import {ServerActionResult} from "@/types/action-type"; import {ZodString} from "zod"; import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert"; -import {AlertCircleIcon, Trash2} from "lucide-react"; -import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm"; +import {AlertCircleIcon} from "lucide-react"; type BackupActionsFormProps = { backup: BackupWith; @@ -42,6 +41,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? [] const isMobile = useIsMobile(); const {closeModal} = useBackupModal(); + const queryClient = useQueryClient(); const form = useZodForm({ schema: BackupActionsSchema, @@ -74,8 +74,8 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { if (inner?.success) { toast.success(inner.actionSuccess?.message); + queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]}); if (action === "download") { - console.log(inner.value) const url = inner.value if (typeof url === "string") { window.open(url, "_self"); @@ -91,6 +91,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { } else { if (action === "delete") { toast.success("Backup deleted successfully.") + queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]}); closeModal() } else { toast.error(inner?.actionError?.message ?? "An error occurred."); @@ -111,6 +112,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { if (inner?.success) { toast.success(inner.actionSuccess?.message); + queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]}); closeModal() } else { toast.error(inner?.actionError?.message); @@ -130,10 +132,7 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { }} > - {filteredBackupStorages.length > 0 ? - - { // > // Delete entire backup // + - - , - variant: "destructive", - onClick: async () => { - mutationDeleteEntireBackup.mutateAsync() - }, - }, - cancel: { - className: "w-full", - text: "Cancel", - icon: , - variant: "outline", - }, - }} + mutationDeleteEntireBackup.mutateAsync()} isPending={mutationDeleteEntireBackup.isPending} - /> + disabled={mutationDeleteEntireBackup.isPending} + > + Delete entire backup + + + // , + // variant: "destructive", + // onClick: async () => { + // mutationDeleteEntireBackup.mutateAsync() + // }, + // }, + // cancel: { + // className: "w-full", + // text: "Cancel", + // icon: , + // variant: "outline", + // }, + // }} + // isPending={mutationDeleteEntireBackup.isPending} + // /> )} 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 index 7a28ae74..99885061 100644 --- a/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts @@ -57,10 +57,8 @@ export const downloadBackupAction = userAction.schema( } }; - console.log(input) const result = await dispatchStorage(input, undefined, backupStorage.storageChannelId); - console.log(result); return { success: result.success, value: result.url, diff --git a/src/components/wrappers/dashboard/database/backup/actions/get-data.action.ts b/src/components/wrappers/dashboard/database/backup/actions/get-data.action.ts new file mode 100644 index 00000000..7e5447bc --- /dev/null +++ b/src/components/wrappers/dashboard/database/backup/actions/get-data.action.ts @@ -0,0 +1,58 @@ +"use server" +import {userAction} from "@/lib/safe-actions/actions"; +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"; + +export const getDatabaseDataAction = userAction + .schema( + z.object({ + databaseId: z.string(), + }) + ) + .action(async ({parsedInput}) => { + const {databaseId} = parsedInput; + + const database = await db.query.database.findFirst({ + where: eq(drizzleDb.schemas.database.id, databaseId), + with: { + project: true, + } + }); + + const backups = 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)], + }) as BackupWith[]; + + const restorations = await db.query.restoration.findMany({ + where: eq(drizzleDb.schemas.restoration.databaseId, databaseId), + orderBy: (r, {desc}) => [desc(r.createdAt)], + }) as Restoration[]; + + const totalBackups = backups.length; + const availableBackups = backups.filter(b => !b.deletedAt).length; + const successfulBackups = backups.filter(b => b.status === "success").length; + const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; + + return { + database, + backups, + restorations, + stats: { + totalBackups, + availableBackups, + successRate + } + }; + }); \ No newline at end of file diff --git a/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx b/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx index cdfa34ea..5fa81aa8 100644 --- a/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx +++ b/src/components/wrappers/dashboard/database/backup/backup-modal-context.tsx @@ -2,7 +2,6 @@ import {createContext, useContext, useState, ReactNode} from "react"; import {BackupWith} from "@/db/schema/07_database"; -import {useRouter} from "next/navigation"; export type DatabaseActionKind = "restore" | "download" | "delete"; @@ -32,7 +31,6 @@ const BackupModalContext = createContext(und export const BackupModalProvider = ({children}: { children: ReactNode }) => { const [open, setOpen] = useState(false); - const router = useRouter(); const [action, setAction] = useState(null); const [backup, setBackup] = useState(null); @@ -46,7 +44,6 @@ export const BackupModalProvider = ({children}: { children: ReactNode }) => { setOpen(false); setAction(null); setBackup(null); - router.refresh() }; return ( diff --git a/src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx b/src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx index 73940aa5..9614f3b5 100644 --- a/src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx +++ b/src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx @@ -8,9 +8,8 @@ import {Button} from "@/components/ui/button"; import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {toast} from "sonner"; -import {useRouter} from "next/navigation"; import {Switch} from "@/components/ui/switch"; import {Card} from "@/components/ui/card"; import Link from "next/link"; @@ -48,7 +47,7 @@ export const ChannelPoliciesForm = ({ onSuccess, kind }: ChannelPoliciesFormProps) => { - const router = useRouter(); + const queryClient = useQueryClient(); const isMobile = useIsMobile(); const channelText = getChannelTextBasedOnKind(kind); @@ -105,10 +104,6 @@ export const ChannelPoliciesForm = ({ (existing.eventKinds !== policy.eventKinds || existing.enabled !== policy.enabled); }); - - console.log(policiesToUpdate); - console.log(policiesToAdd); - const promises = kind === "notification" ? [ policiesToAdd.length > 0 ? await createAlertPoliciesAction({databaseId: database.id, alertPolicies: policiesToAdd}) : null, @@ -133,7 +128,10 @@ export const ChannelPoliciesForm = ({ if (failedActions.length > 0) throw new Error(failedActions[0].data.actionError?.message || "One or more operations failed"); return {success: true}; }, - onSuccess: () => { toast.success("Policies saved successfully"); router.refresh(); }, + onSuccess: () => { + toast.success("Policies saved successfully"); + queryClient.invalidateQueries({queryKey: ["database-data", database.id]}); + }, onError: (error: any) => { toast.error(error.message || "Failed to save policies"); }, }); diff --git a/src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx b/src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx index a2a9a8fa..d1cfc9ae 100644 --- a/src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx +++ b/src/components/wrappers/dashboard/database/channels-policy/policy-modal.tsx @@ -37,7 +37,6 @@ export const ChannelPoliciesModal = ({icon, kind, database, channels, organizati const channelsIds = channelsFiltered .map(channel => channel.id); - console.log(channelsIds); const activeAlertPolicies = database.alertPolicies?.filter((policy) => channelsIds.includes(policy.notificationChannelId)); const activeStoragePolicies = database.storagePolicies?.filter((policy) => channelsIds.includes(policy.storageChannelId)); diff --git a/src/components/wrappers/dashboard/database/cron-button/cron-button.tsx b/src/components/wrappers/dashboard/database/cron-button/cron-button.tsx index 31518cfe..85c0423f 100644 --- a/src/components/wrappers/dashboard/database/cron-button/cron-button.tsx +++ b/src/components/wrappers/dashboard/database/cron-button/cron-button.tsx @@ -7,9 +7,8 @@ import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { useState } from "react"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { useRouter } from "next/navigation"; import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action"; import {Database} from "@/db/schema/07_database"; @@ -18,7 +17,7 @@ export type CronButtonProps = { }; export const CronButton = (props: CronButtonProps) => { - const router = useRouter(); + const queryClient = useQueryClient(); const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null); const [open, setOpen] = useState(false); @@ -27,7 +26,7 @@ export const CronButton = (props: CronButtonProps) => { updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }), onSuccess: () => { toast.success(`Method updated successfully.`); - router.refresh(); + queryClient.invalidateQueries({ queryKey: ["database-data", props.database.id] }); }, onError: () => { toast.error(`An error occurred while updating backup method.`); diff --git a/src/components/wrappers/dashboard/database/cron-button/cron-input.tsx b/src/components/wrappers/dashboard/database/cron-button/cron-input.tsx index 6b0008f8..c8ded057 100644 --- a/src/components/wrappers/dashboard/database/cron-button/cron-input.tsx +++ b/src/components/wrappers/dashboard/database/cron-button/cron-input.tsx @@ -1,8 +1,7 @@ import {AdvancedCronSelect} from "./advanced-cron-select"; import {updateDatabaseBackupPolicyAction} from "@/components/wrappers/dashboard/database/cron-button/cron.action"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {useState} from "react"; -import {useRouter} from "next/navigation"; import {toast} from "sonner"; import {Button} from "@/components/ui/button"; import {Separator} from "@/components/ui/separator"; @@ -15,14 +14,14 @@ export type CronInputProps = { export const CronInput = ({database, onSuccess}: CronInputProps) => { const [cron, setCron] = useState(database.backupPolicy ?? "* * * * *"); - const router = useRouter(); + const queryClient = useQueryClient(); const updateBackupPolicy = useMutation({ mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}), onSuccess: () => { toast.success(`Cron updated successfully.`); onSuccess?.() - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", database.id]}); }, onError: () => { toast.error(`An error occurred while updating cron value.`); diff --git a/src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx b/src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx index 67f49575..2ecd0ce9 100644 --- a/src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx +++ b/src/components/wrappers/dashboard/database/import/upload-backup-zone.tsx @@ -1,10 +1,9 @@ "use client"; import {DropZoneFile} from "@/components/wrappers/common/dropzone/dropzone-file"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {useState} from "react"; import {Loader2} from "lucide-react"; -import {useRouter} from "next/navigation"; import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import {toast} from "sonner"; import {uploadBackupAction} from "@/components/wrappers/dashboard/database/import/upload-backup.action"; @@ -16,7 +15,7 @@ type UploadRetentionZoneProps = { }; export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZoneProps) => { - const router = useRouter(); + const queryClient = useQueryClient(); const [isProcessing, setIsProcessing] = useState(false); const [file, setFile] = useState(null); @@ -37,7 +36,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ if (inner?.success) { toast.success(inner.actionSuccess?.message); onSuccessAction?.() - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", databaseId]}); } else { toast.error(inner?.actionError?.message); } @@ -45,7 +44,7 @@ export const UploadBackupZone = ({onSuccessAction, databaseId}: UploadRetentionZ console.error(err); toast.error("An error occurred while upload in the backup"); } finally { - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", databaseId]}); setIsProcessing(false); } }, diff --git a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx index f596db7b..2e612e67 100644 --- a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx +++ b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings-form.tsx @@ -10,11 +10,10 @@ import { useZodForm } from "@/components/ui/form"; import {RetentionSettings, RetentionSettingsSchema} from "./backup-retention-settings.schema"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {updateOrCreateBackupRetentionPolicyAction} from "./backup-retention-settings.action"; import {DatabaseWith, RetentionPolicy} from "@/db/schema/07_database"; import {toast} from "sonner"; -import {useRouter} from "next/navigation"; import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group"; import {Badge} from "@/components/ui/badge"; import {Separator} from "@/components/ui/separator"; @@ -28,7 +27,7 @@ export type BackupRetentionSettingsFormProps = { }; export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRetentionSettingsFormProps) => { - const router = useRouter(); + const queryClient = useQueryClient(); const defaultValuesFormatted: RetentionSettings = { type: defaultValues?.type, @@ -55,7 +54,7 @@ export const BackupRetentionSettingsForm = ({defaultValues, database}: BackupRet }), onSuccess: () => { toast.success("Retention policy updated successfully."); - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", database.id]}); }, onError: () => { toast.error("An error occurred while updating retention policy."); diff --git a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx index 5f41c41c..45d5bdeb 100644 --- a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx +++ b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx @@ -49,7 +49,7 @@ export function BackupRetentionSettings({database}: BackupRetentionSettingsProps yearly: 3, }, }) - const router = useRouter() + const queryClient = useQueryClient() const updateRetentionPolicy = useMutation({ mutationFn: async (payload: RetentionSettings) => await updateOrCreateBackupRetentionPolicyAction({ @@ -58,7 +58,7 @@ export function BackupRetentionSettings({database}: BackupRetentionSettingsProps }), onSuccess: () => { toast.success("Retention policy updated successfully.") - router.refresh() + queryClient.invalidateQueries({queryKey: ["database-data", database.id]}) }, onError: () => { toast.error("An error occurred while updating retention policy.") diff --git a/src/components/wrappers/dashboard/organization/organization-combobox.tsx b/src/components/wrappers/dashboard/organization/organization-combobox.tsx index 9d3f4898..0c9940b1 100644 --- a/src/components/wrappers/dashboard/organization/organization-combobox.tsx +++ b/src/components/wrappers/dashboard/organization/organization-combobox.tsx @@ -75,7 +75,7 @@ export function OrganizationCombobox() { >
{activeOrganization?.logo ? ( @@ -109,33 +109,33 @@ export function OrganizationCombobox() { className={cn( "group gap-2 p-1 cursor-pointer rounded-lg mb-1 last:mb-0 transition-colors", isActive - ? "bg-orange-500/10 text-orange-600 dark:text-orange-400 border border-orange-500/20" + ? "bg-primary/10 text-primary border border-primary/20" : "focus:bg-accent hover:bg-accent/50 border border-transparent" )}>
{org.logo ? ( {org.name} ) : ( )}
{org.name}
{isActive && (
- + className="ml-auto flex size-5 items-center justify-center rounded-full bg-primary shadow-sm"> +
)} diff --git a/src/components/wrappers/dashboard/profile/profile-account.tsx b/src/components/wrappers/dashboard/profile/profile-account.tsx index e976f93d..1f7ef228 100644 --- a/src/components/wrappers/dashboard/profile/profile-account.tsx +++ b/src/components/wrappers/dashboard/profile/profile-account.tsx @@ -1,6 +1,5 @@ "use client"; -import {useEffect} from "react"; import {Button} from "@/components/ui/button"; import {Input} from "@/components/ui/input"; import {AlertCircle, Loader2} from "lucide-react"; @@ -22,18 +21,6 @@ export function ProfileAccount({user}: ProfileAccountProps) { const router = useRouter(); - useEffect(() => { - let interval: NodeJS.Timeout; - - interval = setInterval(async () => { - router.refresh(); - }, 5000); - - return () => { - if (interval) clearInterval(interval); - }; - }); - const emailForm = useZodForm({ schema: EmailSchema, defaultValues: { 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 39c83bec..60c952ec 100644 --- a/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx @@ -8,11 +8,11 @@ import {DataTable} from "@/components/wrappers/common/table/data-table"; import {useMemo, useState} from "react"; import {Backup, BackupWith, DatabaseWith} from "@/db/schema/07_database"; import {Setting} from "@/db/schema/01_setting"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {toast} from "sonner"; -import {useRouter} from "next/navigation"; import {MemberWithUser} from "@/db/schema/03_organization"; import {deleteBackupAction} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action"; +import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm"; type DatabaseBackupListProps = { @@ -32,7 +32,12 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => { ] const [selectedFilters, setSelectedFilters] = useState([items[1]]); - const router = useRouter(); + const [isActionsOpen, setIsActionsOpen] = useState(false); + const queryClient = useQueryClient(); + + const columns = useMemo(() => { + return backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember); + }, [props.isAlreadyRestore, props.activeMember.id, props.activeMember.role]); const filteredBackups = useMemo(() => { if (!props.backups) return []; @@ -98,7 +103,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => { toast.error(result.message); } }); - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", props.database.id]}); }, }); @@ -107,7 +112,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => { return ( ( @@ -115,28 +120,44 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
{!isMember && ( - + { + onClick={(e) => { + e.stopPropagation(); + setIsActionsOpen(true); }} disabled={rows.length === 0 || mutationDeleteBackups.isPending} icon={} isPending={mutationDeleteBackups.isPending} size="sm" + type="button" >Actions - { - await mutationDeleteBackups.mutateAsync(rows) + { + const backupsToDelete = rows.map(row => row + ).filter(backup => backup.deletedAt == null) + if (backupsToDelete.length === 0) { + toast.error("No available backup selected for deletion."); + return; + } + mutationDeleteBackups.mutate(backupsToDelete); + setIsActionsOpen(false); }} - className="text-red-600 focus:text-red-700" + onCancel={() => setIsActionsOpen(false)} + title="Delete backups?" + description="Are you sure you want to delete the selected backups? This action cannot be undone." + confirmButtonText="Yes, delete" + cancelButtonText="Cancel" > - - Delete Selected - + e.preventDefault()}> + + Delete Selected + + )} diff --git a/src/components/wrappers/dashboard/projects/database/database-content.tsx b/src/components/wrappers/dashboard/projects/database/database-content.tsx index 6f76298b..3413df1a 100644 --- a/src/components/wrappers/dashboard/projects/database/database-content.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-content.tsx @@ -5,6 +5,17 @@ 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"; +import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi"; +import {useQuery, useQueryClient} from "@tanstack/react-query"; +import {getDatabaseDataAction} from "@/components/wrappers/dashboard/database/backup/actions/get-data.action"; +import {PageContent, PageDescription, PageTitle} from "@/features/layout/page"; +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 {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal"; +import {HardDrive, Megaphone} from "lucide-react"; +import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal"; +import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; export type DatabaseContentProps = { @@ -13,27 +24,114 @@ export type DatabaseContentProps = { restorations: Restoration[], isAlreadyRestore: boolean, database: DatabaseWith, - activeMember: MemberWithUser + activeMember: MemberWithUser, + totalBackups: number, + availableBackups: number, + successRate: number | null, + organizationId: string, + activeOrganizationChannels: any[], + activeOrganizationStorageChannels: any[] } -export const DatabaseContent = ({ - settings, - backups, - activeMember, - isAlreadyRestore, - restorations, - database - }: DatabaseContentProps) => { +export const DatabaseContent = (props: DatabaseContentProps) => { const {} = useBackupModal(); + const {data} = useQuery({ + queryKey: ["database-data", props.database.id], + queryFn: async () => { + const result = await getDatabaseDataAction({databaseId: props.database.id}); + return result?.data; + }, + initialData: { + database: { + ...props.database, + project: props.database.project ?? null, + }, + backups: props.backups, + restorations: props.restorations, + stats: { + totalBackups: props.totalBackups, + availableBackups: props.availableBackups, + successRate: props.successRate + } + }, + staleTime: 0, + gcTime: 0, + refetchInterval: 5000, + }); + + const database = data?.database ?? props.database; + const backups = data?.backups ?? props.backups; + const restorations = data?.restorations ?? props.restorations; + const stats = data?.stats ?? { + totalBackups: props.totalBackups, + availableBackups: props.availableBackups, + successRate: props.successRate + }; + + const isAlreadyRestore = restorations.some((r) => r.status === "waiting"); + const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing"); + + const isMember = props.activeMember.role === "member"; + return ( <> - - +
+ +
+ {capitalizeFirstLetter(database.name)} +
+ {!isMember && ( +
+
+ + + } + channels={props.activeOrganizationChannels} + organizationId={props.organizationId} + /> + } + kind={"storage"} + channels={props.activeOrganizationStorageChannels} + organizationId={props.organizationId} + /> + +
+
+ +
+
+ )} +
+
+ + {database.description && ( + {database.description} + )} + + + + + + ) } \ No newline at end of file diff --git a/src/components/wrappers/dashboard/projects/database/database-kpi.tsx b/src/components/wrappers/dashboard/projects/database/database-kpi.tsx index 2a3c4ab9..00dc74a9 100644 --- a/src/components/wrappers/dashboard/projects/database/database-kpi.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-kpi.tsx @@ -11,7 +11,6 @@ export type DatabaseKpiPro = { totalBackups: number; availableBackups: number; }; - export const DatabaseKpi = (props: DatabaseKpiPro) => { return (
@@ -42,7 +41,7 @@ export const DatabaseKpi = (props: DatabaseKpiPro) => {
- {props.successRate ? `${props.successRate.toFixed(0)} %` : "Unavailable"} + {typeof props.successRate === 'number' ? `${props.successRate.toFixed(0)} %` : "Unavailable"}

Backup success rate

diff --git a/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx b/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx index 1c0db086..271a3760 100644 --- a/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx @@ -5,21 +5,28 @@ import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import {MoreHorizontal, Trash2} from "lucide-react"; import {Restoration} from "@/db/schema/07_database"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action"; import {toast} from "sonner"; -import {useRouter} from "next/navigation"; import {MemberWithUser} from "@/db/schema/03_organization"; +import {useMemo, useState} from "react"; +import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm"; type DatabaseRestoreListProps = { isAlreadyRestore: boolean; restorations: Restoration[]; - activeMember: MemberWithUser + activeMember: MemberWithUser; + databaseId: string; } export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => { - const router = useRouter(); + const queryClient = useQueryClient(); + const [isActionsOpen, setIsActionsOpen] = useState(false); + + const columns = useMemo(() => { + return restoreColumns(props.isAlreadyRestore, props.activeMember); + }, [props.isAlreadyRestore, props.activeMember.id, props.activeMember.role]); const mutationDeleteRestorations = useMutation({ mutationFn: async (restorations: Restoration[]) => { @@ -46,7 +53,7 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => { toast.error(result.message); } }); - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]}); }, }); const isMember = props.activeMember.role === "member"; @@ -55,35 +62,48 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => { return ( ( <> {!isMember && ( - + { + onClick={(e) => { + e.stopPropagation(); + setIsActionsOpen(true); }} disabled={rows.length === 0 || mutationDeleteRestorations.isPending} icon={} isPending={mutationDeleteRestorations.isPending} size="sm" + type="button" >Actions - { - await mutationDeleteRestorations.mutateAsync(rows) + { + mutationDeleteRestorations.mutate(rows) + setIsActionsOpen(false); }} - disabled={props.isAlreadyRestore} - className="text-red-600 focus:text-red-700" + onCancel={() => setIsActionsOpen(false)} + title="Delete restorations?" + description="Are you sure you want to delete the selected restorations? This action cannot be undone." + confirmButtonText="Yes, delete" + cancelButtonText="Cancel" > - - Delete Selected - + e.preventDefault()} + className="text-red-600 focus:text-red-700" + > + + Delete Selected + + )} diff --git a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx index 7223723c..fc8c965e 100644 --- a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx @@ -3,7 +3,6 @@ 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 {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"; @@ -25,21 +24,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => { const [tab, setTab] = useState(() => searchParams.get("tab") ?? "backup"); - useEffect(() => { - const eventSource = new EventSource("/api/events"); - eventSource.addEventListener("modification", (event) => { - const data: eventUpdate = JSON.parse(event.data); - if (data.update) { - router.refresh(); - } - }); - - return () => { - eventSource.close(); - }; - }, []); - - useEffect(() => { const newTab = searchParams.get("tab") ?? "backup"; setTab(newTab); @@ -69,6 +53,7 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => { isAlreadyRestore={props.isAlreadyRestore} restorations={props.restorations} activeMember={props.activeMember} + databaseId={props.database.id} /> diff --git a/src/components/wrappers/dashboard/projects/project-card/project-card.tsx b/src/components/wrappers/dashboard/projects/project-card/project-card.tsx index 9a84bfd5..9208a806 100644 --- a/src/components/wrappers/dashboard/projects/project-card/project-card.tsx +++ b/src/components/wrappers/dashboard/projects/project-card/project-card.tsx @@ -18,11 +18,11 @@ export const ProjectCard = (props: projectCardProps) => { return ( - +
-
+
{dbCount} {dbCount === 1 ? "Database" : "Databases"} @@ -31,7 +31,7 @@ export const ProjectCard = (props: projectCardProps) => {
-

+

{project.name}

@@ -40,8 +40,8 @@ export const ProjectCard = (props: projectCardProps) => {

- View Project -
+ View Project +
Details
diff --git a/src/components/wrappers/dashboard/statistics/charts/percentage-line-chart.tsx b/src/components/wrappers/dashboard/statistics/charts/percentage-line-chart.tsx index 8c5644ae..66d8f928 100644 --- a/src/components/wrappers/dashboard/statistics/charts/percentage-line-chart.tsx +++ b/src/components/wrappers/dashboard/statistics/charts/percentage-line-chart.tsx @@ -83,7 +83,7 @@ export function PercentageLineChart(props: percentageLineChartProps) { `${tick}%`} + tickFormatter={(tick) => `${Number(tick).toFixed(0)}%`} /> Success Rate : - {data.successRate} % + {data.successRate.toFixed(0)} %
diff --git a/src/features/agents/agents.action.ts b/src/features/agents/agents.action.ts index 93fa782a..63cbc1b9 100644 --- a/src/features/agents/agents.action.ts +++ b/src/features/agents/agents.action.ts @@ -46,3 +46,15 @@ export const updateAgentAction = userAction data: updatedAgent, }; }); + +export const getAgentAction = userAction.schema(z.string()).action(async ({parsedInput}) => { + const agent = await db.query.agent.findFirst({ + where: eq(drizzleDb.schemas.agent.id, parsedInput), + with: { + databases: true + } + }); + return { + data: agent, + }; +}); diff --git a/src/features/agents/components/agent.form.tsx b/src/features/agents/components/agent.form.tsx index 3f0cdee2..a1250ec2 100644 --- a/src/features/agents/components/agent.form.tsx +++ b/src/features/agents/components/agent.form.tsx @@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input"; import {Form} from "@/components/ui/form"; import {Button} from "@/components/ui/button"; import {useRouter} from "next/navigation"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import {TooltipProvider} from "@/components/ui/tooltip"; import {AgentSchema, AgentType} from "@/features/agents/agents.schema"; import {toast} from "sonner"; @@ -27,6 +27,7 @@ export type agentFormProps = { export const AgentForm = (props: agentFormProps) => { const isCreate = !Boolean(props.defaultValues); + const queryClient = useQueryClient(); const form = useZodForm({ schema: AgentSchema, @@ -51,7 +52,10 @@ export const AgentForm = (props: agentFormProps) => { return; } toast.success(`Success ${isCreate ? "creating" : "updating"} agent`); - router.refresh(); + + if (!isCreate && props.agentId) { + queryClient.invalidateQueries({ queryKey: ["agent-data", props.agentId] }); + } if (props.onSuccess) { props.onSuccess(data); diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx index 8422e6d6..ba8aaa9e 100644 --- a/src/features/dashboard/backup/columns.tsx +++ b/src/features/dashboard/backup/columns.tsx @@ -1,39 +1,16 @@ "use client"; import {ColumnDef} from "@tanstack/react-table"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import {Button} from "@/components/ui/button"; -import {Download, MoreHorizontal, Trash2} from "lucide-react"; -import {ReloadIcon} from "@radix-ui/react-icons"; -import { - getFileUrlPresignedLocal, - getFileUrlPreSignedS3Action -} from "@/features/upload/private/upload.action"; -import {useMutation} from "@tanstack/react-query"; -import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/restore/restore.action"; -import {toast} from "sonner"; -import {useRouter} from "next/navigation"; import {StatusBadge} from "@/components/wrappers/common/status-badge"; import {Backup, DatabaseWith} from "@/db/schema/07_database"; -import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom"; import {Setting} from "@/db/schema/01_setting"; -import {SafeActionResult} from "next-safe-action"; -import {ZodString} from "zod"; -import {ServerActionResult} from "@/types/action-type"; import {cn} from "@/lib/utils"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; 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"; - +import { Badge as BadgeC } from "@/components/ui/badge"; export function backupColumns( isAlreadyRestore: boolean, @@ -68,7 +45,16 @@ export function backupColumns( cell: ({row}) => { const reference = row.original.id const isImported = row.original.imported - return isImported ? `${reference} (imported)` : `${reference}` + return ( +
+ {reference} + {isImported && ( + + Imported + + )} +
+ ) }, }, { diff --git a/src/features/dashboard/restore/columns.tsx b/src/features/dashboard/restore/columns.tsx index 7256b00f..715bafde 100644 --- a/src/features/dashboard/restore/columns.tsx +++ b/src/features/dashboard/restore/columns.tsx @@ -15,13 +15,12 @@ import { ReloadIcon } from "@radix-ui/react-icons"; import { StatusBadge } from "@/components/wrappers/common/status-badge"; import {Restoration} from "@/db/schema/07_database"; import {formatLocalizedDate} from "@/utils/date-formatting"; -import {useMutation} from "@tanstack/react-query"; +import {useMutation, useQueryClient} from "@tanstack/react-query"; import { deleteRestoreAction, rerunRestorationAction } from "@/features/dashboard/restore/restore.action"; import {toast} from "sonner"; -import {useRouter} from "next/navigation"; import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom"; import {MemberWithUser} from "@/db/schema/03_organization"; @@ -54,7 +53,7 @@ export function restoreColumns( cell: ({ row }) => { const status = row.getValue("status"); - const router = useRouter(); + const queryClient = useQueryClient(); const rowData: Restoration = row.original; @@ -67,7 +66,7 @@ export function restoreColumns( if (restoration.data.success) { // @ts-ignore toast.success(restoration.data.actionSuccess.message); - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]}); } else { // @ts-ignore toast.error(restoration.data.actionError.message); @@ -84,7 +83,7 @@ export function restoreColumns( if (restoration.data.success) { // @ts-ignore toast.success(restoration.data.actionSuccess.message); - router.refresh(); + queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]}); } else { // @ts-ignore toast.error(restoration.data.actionError.message); @@ -109,7 +108,7 @@ export function restoreColumns( {activeMember.role != "member" && ( - diff --git a/src/features/projects/components/project.form.tsx b/src/features/projects/components/project.form.tsx index 258c77ee..67b8726a 100644 --- a/src/features/projects/components/project.form.tsx +++ b/src/features/projects/components/project.form.tsx @@ -4,7 +4,7 @@ import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessa import { Input } from "@/components/ui/input"; import { Form } from "@/components/ui/form"; import { Button } from "@/components/ui/button"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ProjectSchema, ProjectType } from "@/features/projects/projects.schema"; import { createProjectAction, updateProjectAction } from "@/features/projects/projects.action"; import { useRouter } from "next/navigation"; @@ -29,6 +29,7 @@ export type projectFormProps = { export const ProjectForm = (props: projectFormProps) => { const router = useRouter(); + const queryClient = useQueryClient(); const isCreate = !Boolean(props.defaultValues); const formatDatabasesList = (databases: DatabaseWith[]) => { return databases.map((database) => ({ @@ -70,7 +71,13 @@ export const ProjectForm = (props: projectFormProps) => { if (project && project.data) { if (project.data.success) { project.data.actionSuccess && toast.success(project.data.actionSuccess.message); - router.refresh(); + + if (!isCreate && values.databases) { + values.databases.forEach(dbId => { + queryClient.invalidateQueries({ queryKey: ["database-data", dbId] }); + }); + } + if (props.onSuccess) { props.onSuccess(project.data.value); } else { @@ -78,11 +85,9 @@ export const ProjectForm = (props: projectFormProps) => { } } else { project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred."); - router.refresh(); } } else { toast.error("Failed to process request. No response received."); - router.refresh(); } }, }); diff --git a/src/features/upload/private/upload.action.ts b/src/features/upload/private/upload.action.ts index 6388262a..03642236 100644 --- a/src/features/upload/private/upload.action.ts +++ b/src/features/upload/private/upload.action.ts @@ -75,10 +75,7 @@ export async function deleteFileS3Private(fileName: string, bucketName: string) */ export async function deleteLocalPrivate(fileName: string) { try { - console.log(fileName); const filePath = path.join(process.cwd(), privateLocalDir, fileName); - console.log(`Deleted ${filePath}`); - // Delete locally await unlink(filePath); return { diff --git a/src/hooks/use-auto-refresh.tsx b/src/hooks/use-auto-refresh.tsx deleted file mode 100644 index 5574ee31..00000000 --- a/src/hooks/use-auto-refresh.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import {useEffect, useRef} from "react"; -import {useRouter} from "next/navigation"; - -type UseAutoRefreshOptions = { - poll?: { - intervalMs: number; - enabled?: boolean; - }; - sse?: { - url: string; - eventName?: string; - enabled?: boolean; - shouldRefresh?: (data: unknown) => boolean; - }; -}; - -export function useAutoRefresh(options: UseAutoRefreshOptions) { - const router = useRouter(); - const eventSourceRef = useRef(null); - - useEffect(() => { - if (!options.poll?.enabled) return; - if (!options.poll.intervalMs) return; - - const interval = setInterval(() => { - router.refresh(); - }, options.poll.intervalMs); - - return () => clearInterval(interval); - }, [options.poll?.enabled, options.poll?.intervalMs, router]); - - useEffect(() => { - if (!options.sse?.enabled) return; - if (!options.sse.url) return; - - const eventSource = new EventSource(options.sse.url); - eventSourceRef.current = eventSource; - - const eventName = options.sse.eventName ?? "message"; - - eventSource.addEventListener(eventName, (event: MessageEvent) => { - let payload: unknown = event.data; - - try { - payload = JSON.parse(event.data); - } catch {} - - const shouldRefresh = - options.sse?.shouldRefresh?.(payload) ?? true; - - if (shouldRefresh) { - router.refresh(); - } - }); - - return () => { - eventSource.close(); - eventSourceRef.current = null; - }; - }, [ - options.sse?.enabled, - options.sse?.url, - options.sse?.eventName, - router, - ]); -} diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index fa7fc10d..81dfcb48 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -517,7 +517,6 @@ export const getActiveMember = async () => { const member = await auth.api.getActiveMember({ headers: await headers(), }); - console.log(member); return member as MemberWithUser; } catch (e) { diff --git a/src/lib/tasks/database/index.ts b/src/lib/tasks/database/index.ts index d3581041..6ee92a26 100644 --- a/src/lib/tasks/database/index.ts +++ b/src/lib/tasks/database/index.ts @@ -5,7 +5,6 @@ import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf"; import {retentionPolicy} from "@/db/schema/07_database"; import {isNull} from "drizzle-orm"; import * as drizzleDb from "@/db"; -import {eventEmitter} from "../../../../app/api/events/route"; export const retentionCleanTask = async () => { @@ -52,5 +51,4 @@ export async function enforceRetention( }); break; } - eventEmitter.emit('modification', {update: true}); } diff --git a/src/types/events.ts b/src/types/events.ts deleted file mode 100644 index 445ee67b..00000000 --- a/src/types/events.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type eventUpdate = { - update: boolean; -};