EMAILOUT / frontend /src /pages /RunHistory.jsx
Seth
update
c356b87
import React, { useState, useEffect } from 'react';
import { History, CheckCircle2, XCircle, Clock, ExternalLink, Filter } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { motion } from 'framer-motion';
export default function RunHistory() {
const [runs, setRuns] = useState([]);
const [loading, setLoading] = useState(true);
const [filterStatus, setFilterStatus] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
fetchRuns();
}, []);
const fetchRuns = async () => {
try {
setLoading(true);
const response = await fetch('/api/smartlead-runs');
if (response.ok) {
const data = await response.json();
setRuns(data);
}
} catch (error) {
console.error('Error fetching runs:', error);
} finally {
setLoading(false);
}
};
const filteredRuns = runs.filter(run => {
const matchesStatus = filterStatus === 'all' || run.status === filterStatus;
const matchesSearch = searchQuery === '' ||
run.campaign_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
run.campaign_id?.toLowerCase().includes(searchQuery.toLowerCase()) ||
run.run_id?.toLowerCase().includes(searchQuery.toLowerCase());
return matchesStatus && matchesSearch;
});
const getStatusBadge = (status) => {
const variants = {
'completed': 'bg-green-100 text-green-700',
'failed': 'bg-red-100 text-red-700',
'pending': 'bg-yellow-100 text-yellow-700',
'dry_run_completed': 'bg-blue-100 text-blue-700'
};
return variants[status] || 'bg-slate-100 text-slate-700';
};
const getStatusIcon = (status) => {
if (status === 'completed' || status === 'dry_run_completed') {
return <CheckCircle2 className="h-4 w-4" />;
} else if (status === 'failed') {
return <XCircle className="h-4 w-4" />;
} else {
return <Clock className="h-4 w-4" />;
}
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString();
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
{/* Header */}
<header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-50">
<div className="max-w-6xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600
flex items-center justify-center shadow-lg shadow-violet-200">
<History className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="font-bold text-slate-800 text-lg">Run History</h1>
<p className="text-xs text-slate-500">Smartlead campaign push history</p>
</div>
</div>
<Button
variant="outline"
onClick={() => window.location.href = '/'}
className="text-slate-500 hover:text-slate-700"
>
Back to Generator
</Button>
</div>
</div>
</header>
<main className="max-w-6xl mx-auto px-6 py-8">
{/* Filters */}
<div className="mb-6 flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Input
placeholder="Search by campaign name, ID, or run ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-full sm:w-48">
<Filter className="h-4 w-4 mr-2 text-slate-400" />
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="dry_run_completed">Dry Run</SelectItem>
</SelectContent>
</Select>
</div>
{/* Runs List */}
{loading ? (
<div className="text-center py-16">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-violet-600"></div>
<p className="text-sm text-slate-500 mt-2">Loading runs...</p>
</div>
) : filteredRuns.length === 0 ? (
<div className="text-center py-16">
<div className="mx-auto w-16 h-16 rounded-2xl bg-slate-100 flex items-center justify-center mb-4">
<History className="h-8 w-8 text-slate-300" />
</div>
<h3 className="text-lg font-semibold text-slate-400 mb-2">No runs found</h3>
<p className="text-sm text-slate-400">Push sequences to Smartlead to see history here</p>
</div>
) : (
<div className="space-y-3">
{filteredRuns.map((run, index) => (
<motion.div
key={run.run_id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="rounded-xl border border-slate-200 bg-white p-5 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4 flex-1">
<div className={`rounded-lg p-2 ${getStatusBadge(run.status)}`}>
{getStatusIcon(run.status)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold text-slate-800">
{run.campaign_name || 'Unnamed Campaign'}
</h4>
<Badge className={getStatusBadge(run.status)}>
{run.status}
</Badge>
{run.dry_run && (
<Badge variant="outline" className="text-xs">
Dry Run
</Badge>
)}
</div>
<div className="flex flex-wrap items-center gap-4 text-sm text-slate-500">
{run.campaign_id && (
<span className="font-mono text-xs">
Campaign: {run.campaign_id}
</span>
)}
<span>
{run.mode === 'new' ? 'New Campaign' : 'Existing Campaign'}
</span>
<span>
{run.steps_count} {run.steps_count === 1 ? 'step' : 'steps'}
</span>
</div>
</div>
</div>
<div className="text-right text-xs text-slate-500">
<div>{formatDate(run.created_at)}</div>
{run.completed_at && (
<div className="text-slate-400 mt-1">
Completed: {formatDate(run.completed_at)}
</div>
)}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 pt-4 border-t border-slate-100">
<div>
<div className="text-xs text-slate-500 mb-1">Total Leads</div>
<div className="text-lg font-semibold text-slate-800">{run.total_leads}</div>
</div>
<div>
<div className="text-xs text-slate-500 mb-1">Added</div>
<div className="text-lg font-semibold text-green-600">{run.added_leads}</div>
</div>
{run.skipped_leads > 0 && (
<div>
<div className="text-xs text-slate-500 mb-1">Skipped</div>
<div className="text-lg font-semibold text-yellow-600">{run.skipped_leads}</div>
</div>
)}
{run.failed_leads > 0 && (
<div>
<div className="text-xs text-slate-500 mb-1">Failed</div>
<div className="text-lg font-semibold text-red-600">{run.failed_leads}</div>
</div>
)}
</div>
{/* Run ID */}
<div className="mt-3 pt-3 border-t border-slate-100">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-mono">
Run ID: {run.run_id}
</span>
{run.campaign_id && (
<Button
variant="ghost"
size="sm"
onClick={() => {
// TODO: Open Smartlead campaign URL if available
window.open(`https://app.smartlead.ai/campaigns/${run.campaign_id}`, '_blank');
}}
className="h-7 text-xs"
>
<ExternalLink className="h-3 w-3 mr-1" />
View in Smartlead
</Button>
)}
</div>
</div>
</motion.div>
))}
</div>
)}
</main>
</div>
);
}