baveshraam's picture
FIX: SurrealDB 2.0 migration syntax and Frontend/CORS link
f871fed
'use client';
/**
* Activity Feed Component
*
* Shows a unified timeline of all monitoring activity including
* notifications, job runs, and monitor status changes.
*/
import { useMemo } from 'react';
import {
Bell, CheckCircle, XCircle, RefreshCw, Clock,
AlertTriangle, Info, AlertCircle, Eye, Zap, Activity
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useTriggerMonitoringJob } from '@/lib/hooks/use-monitoring';
import type { UpdateNotification, MonitorJobRun, SourceMonitor } from '@/lib/types/monitoring';
interface ActivityFeedProps {
notifications: UpdateNotification[];
jobs: MonitorJobRun[];
monitors: SourceMonitor[];
isLoading?: boolean;
}
interface ActivityItem {
id: string;
type: 'notification' | 'job' | 'monitor';
timestamp: Date;
title: string;
description: string;
icon: React.ReactNode;
severity?: 'info' | 'warning' | 'critical' | 'success' | 'error';
metadata?: Record<string, any>;
}
function getSeverityColor(severity?: string) {
switch (severity) {
case 'critical':
case 'error':
return 'text-destructive bg-destructive/10';
case 'warning':
return 'text-yellow-500 bg-yellow-500/10';
case 'success':
return 'text-green-500 bg-green-500/10';
case 'info':
default:
return 'text-blue-500 bg-blue-500/10';
}
}
function getNotificationIcon(severity: string) {
switch (severity) {
case 'critical':
return <AlertCircle className="h-4 w-4" />;
case 'warning':
return <AlertTriangle className="h-4 w-4" />;
default:
return <Info className="h-4 w-4" />;
}
}
function formatTimeAgo(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
export function ActivityFeed({ notifications, jobs, monitors, isLoading }: ActivityFeedProps) {
const triggerJob = useTriggerMonitoringJob();
// Combine all activities into a unified timeline
const activities = useMemo(() => {
const items: ActivityItem[] = [];
// Add notifications
notifications.forEach(n => {
items.push({
id: `notification-${n.id}`,
type: 'notification',
timestamp: new Date(n.created_at),
title: n.source_title,
description: n.change_summary,
icon: getNotificationIcon(n.severity),
severity: n.severity,
metadata: {
isRead: n.is_read,
diffHighlights: n.diff_highlights,
},
});
});
// Add job runs
jobs.forEach(j => {
const status = j.status;
items.push({
id: `job-${j.id}`,
type: 'job',
timestamp: new Date(j.started_at),
title: `Monitoring check ${status}`,
description: `Checked ${j.sources_checked} sources, found ${j.updates_found} updates`,
icon: status === 'completed'
? <CheckCircle className="h-4 w-4" />
: status === 'failed'
? <XCircle className="h-4 w-4" />
: <RefreshCw className="h-4 w-4 animate-spin" />,
severity: status === 'completed' ? 'success' : status === 'failed' ? 'error' : 'info',
metadata: {
duration: j.completed_at
? `${Math.round((new Date(j.completed_at).getTime() - new Date(j.started_at).getTime()) / 1000)}s`
: 'In progress',
errors: j.errors,
},
});
});
// Sort by timestamp, newest first
return items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}, [notifications, jobs]);
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Activity Feed
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="flex gap-4">
<div className="h-8 w-8 rounded-full bg-muted animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-4 w-1/3 bg-muted rounded animate-pulse" />
<div className="h-3 w-2/3 bg-muted rounded animate-pulse" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
if (activities.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Activity Feed
</CardTitle>
<CardDescription>
Real-time updates from your source monitors
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<Activity className="h-8 w-8 text-primary" />
</div>
<p className="font-medium mb-1">No activity yet</p>
<p className="text-sm text-muted-foreground mb-4">
{monitors.length === 0
? 'Add some monitors to start tracking source changes'
: 'Run a check to see activity here'}
</p>
{monitors.length > 0 && (
<Button onClick={() => triggerJob.mutate(undefined)} disabled={triggerJob.isPending}>
{triggerJob.isPending ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Zap className="h-4 w-4 mr-2" />
)}
Run First Check
</Button>
)}
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Activity Feed
</CardTitle>
<CardDescription>
Real-time updates from your source monitors
</CardDescription>
</div>
<Badge variant="secondary">
{activities.length} events
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="relative">
{/* Timeline line */}
<div className="absolute left-4 top-0 bottom-0 w-px bg-border" />
{/* Activity items */}
<div className="space-y-6">
{activities.map((activity, index) => (
<div key={activity.id} className="relative flex gap-4 pl-10">
{/* Timeline dot */}
<div
className={`absolute left-0 w-8 h-8 rounded-full flex items-center justify-center ${getSeverityColor(activity.severity)}`}
>
{activity.icon}
</div>
{/* Content */}
<div className="flex-1 min-w-0 pt-0.5">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium truncate">{activity.title}</span>
<Badge
variant={
activity.type === 'notification'
? activity.severity === 'critical' ? 'destructive' : 'secondary'
: activity.severity === 'success' ? 'default'
: activity.severity === 'error' ? 'destructive'
: 'secondary'
}
className="text-xs"
>
{activity.type === 'notification' ? activity.severity : activity.type}
</Badge>
<span className="text-xs text-muted-foreground ml-auto flex-shrink-0">
{formatTimeAgo(activity.timestamp)}
</span>
</div>
<p className="text-sm text-muted-foreground">{activity.description}</p>
{/* Additional metadata */}
{activity.type === 'notification' && activity.metadata && activity.metadata.diffHighlights && activity.metadata.diffHighlights.length > 0 && (
<div className="mt-2 p-2 bg-muted rounded text-xs">
<ul className="space-y-1">
{(activity.metadata.diffHighlights as string[]).slice(0, 2).map((highlight: string, i: number) => (
<li key={i} className="truncate">{highlight}</li>
))}
</ul>
</div>
)}
{activity.type === 'job' && activity.metadata && activity.metadata.errors && activity.metadata.errors.length > 0 && (
<div className="mt-2 p-2 bg-destructive/10 rounded text-xs text-destructive">
{(activity.metadata.errors as string[]).length} error(s) occurred
</div>
)}
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
}