Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/internal/notification/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type NotificationList struct {
Message string `json:"message"`
Type NotificationType `json:"type"`
Status NotificationStatus `json:"status"`
Metadata json.RawMessage `json:"metadata,omitempty"`
CreatedAt string `json:"created_at"`
}

Expand Down
4 changes: 2 additions & 2 deletions apps/api/internal/notification/notification_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (r *NotificationRepository) CreateNotification(n *Notification) error {

func (r *NotificationRepository) GetUserNotifications(userID uuid.UUID) ([]*NotificationList, error) {
query := `
SELECT id, title, message, type, status, created_at
SELECT id, title, message, type, status, metadata, created_at
FROM notifications
WHERE user_id = $1
AND (status = 'unread' OR created_at > datetime('now', '-7 days'))
Expand All @@ -45,7 +45,7 @@ func (r *NotificationRepository) GetUserNotifications(userID uuid.UUID) ([]*Noti
var notifications []*NotificationList
for rows.Next() {
n := &NotificationList{}
err := rows.Scan(&n.ID, &n.Title, &n.Message, &n.Type, &n.Status, &n.CreatedAt)
err := rows.Scan(&n.ID, &n.Title, &n.Message, &n.Type, &n.Status, &n.Metadata, &n.CreatedAt)
if err != nil {
return nil, err
}
Expand Down
140 changes: 140 additions & 0 deletions apps/web/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client"

import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

const Sheet = SheetPrimitive.Root

const SheetTrigger = SheetPrimitive.Trigger

const SheetClose = SheetPrimitive.Close

const SheetPortal = SheetPrimitive.Portal

const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName

const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)

interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName

const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"

const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"

const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName

const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName

export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
192 changes: 192 additions & 0 deletions apps/web/components/views/history/backup-details-sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
'use client';

import { Database, Calendar, Clock, AlertCircle } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import type { Backup } from '@/types/backup';
import { useNotifications } from '@/hooks/use-notifications';
import { useConnections } from '@/hooks/use-connections';
import { useEffect, useState } from 'react';

interface BackupDetailsSheetProps {
backup: Backup | null;
open: boolean;
onClose: () => void;
}

export function BackupDetailsSheet({ backup, open, onClose }: BackupDetailsSheetProps) {
const { notifications } = useNotifications();
const { connections } = useConnections();
const [relatedNotification, setRelatedNotification] = useState<any>(null);
const [connectionName, setConnectionName] = useState<string>('Unknown Connection');

useEffect(() => {
if (backup && connections) {
const connection = connections.find((c: any) => c.id === backup.connection_id);
setConnectionName(connection?.name || 'Unknown Connection');
}
}, [backup, connections]);

useEffect(() => {
if (backup && backup.status === 'failed' && notifications) {
// Find notification related to this backup
const notification = notifications.find(
(n: any) =>
n.type === 'backup_failed' &&
n.metadata?.connection_id === backup.connection_id &&
Math.abs(new Date(n.created_at).getTime() - new Date(backup.created_at).getTime()) < 5000
);
setRelatedNotification(notification);
}
}, [backup, notifications]);

if (!backup) return null;

return (
<Sheet open={open} onOpenChange={(isOpen) => {
if (!isOpen) {
onClose();
}
}}>
<SheetContent className="w-full sm:max-w-[500px]">
<SheetHeader>
<div className="flex items-center gap-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-lg ${
backup.status === 'failed' ? 'bg-destructive/10' : 'bg-primary/10'
}`}>
<Database className={`h-5 w-5 ${
backup.status === 'failed' ? 'text-destructive' : 'text-primary'
}`} />
</div>
<div>
<SheetTitle>Backup Error Details</SheetTitle>
<SheetDescription>{connectionName}</SheetDescription>
</div>
</div>
</SheetHeader>

<ScrollArea className="h-[calc(100vh-120px)] mt-6">
<div className="space-y-6 pr-4">
{/* Status */}
<div>
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Status
</label>
<div className="mt-2">
<Badge variant="destructive" className="text-sm">
<AlertCircle className="mr-1.5 h-3.5 w-3.5" />
Failed
</Badge>
</div>
</div>

<Separator />

{/* Error Message */}
{relatedNotification && (
<>
<div className="space-y-4">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-destructive" />
Error Message
</label>
<div className="rounded-lg bg-destructive/5 border border-destructive/20 p-4">
<p className="text-sm text-foreground leading-relaxed">
{relatedNotification.message}
</p>
{relatedNotification.metadata?.error && (
<pre className="mt-3 text-xs font-mono bg-background/50 p-3 rounded border border-destructive/10 overflow-x-auto whitespace-pre-wrap break-words">
{relatedNotification.metadata.error}
</pre>
)}
</div>
</div>

<Separator />
</>
)}

{/* Database Info */}
<div className="space-y-4">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Database Information
</label>
<div className="space-y-3">
<div className="flex items-start justify-between">
<span className="text-sm text-muted-foreground">Connection</span>
<span className="text-sm font-medium text-right">
{connectionName}
</span>
</div>
{backup.database_name && (
<div className="flex items-start justify-between">
<span className="text-sm text-muted-foreground">Database</span>
<span className="text-sm font-medium font-mono text-right">
{backup.database_name}
</span>
</div>
)}
</div>
</div>

<Separator />

{/* Timing Info */}
<div className="space-y-4">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Timing
</label>
<div className="space-y-3">
<div className="flex items-start justify-between">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<Calendar className="h-4 w-4" />
Started
</span>
<span className="text-sm font-medium text-right">
{new Date(backup.started_time).toLocaleString()}
</span>
</div>
{backup.completed_time && (
<div className="flex items-start justify-between">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<Clock className="h-4 w-4" />
Failed At
</span>
<span className="text-sm font-medium text-right">
{new Date(backup.completed_time).toLocaleString()}
</span>
</div>
)}
</div>
</div>

{/* Path */}
{backup.path && (
<>
<Separator />
<div className="space-y-4">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Attempted Path
</label>
<div className="rounded-lg bg-muted/50 p-3">
<code className="text-xs font-mono text-foreground/80 break-all">
{backup.path}
</code>
</div>
</div>
</>
)}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}
Loading
Loading