- Feat: Add Gotenberg service to docker-compose for headless PDF rendering - Feat: Implement /generate-pdf endpoint in report-generator-service - Feat: Add PDF generation proxy route in api-gateway - Refactor(frontend): Rewrite PDFExportButton to generate HTML with embedded styles and images - Feat(frontend): Auto-crop React Flow screenshots to remove whitespace - Style: Optimize report print layout with CSS (margins, image sizing) - Chore: Remove legacy react-pdf code and font files
154 lines
6.4 KiB
TypeScript
154 lines
6.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Loader2, ArrowRight, History, RefreshCw, Trash2 } from "lucide-react";
|
|
import { WorkflowHistorySummaryDto } from '@/api/schema.gen';
|
|
import { z } from 'zod';
|
|
import { client } from '@/api/client';
|
|
import { useAnalysisTemplates } from "@/hooks/useConfig";
|
|
|
|
type WorkflowHistorySummary = z.infer<typeof WorkflowHistorySummaryDto>;
|
|
|
|
export function RecentWorkflowsList() {
|
|
const [history, setHistory] = useState<WorkflowHistorySummary[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const navigate = useNavigate();
|
|
const { data: templates } = useAnalysisTemplates();
|
|
|
|
const fetchHistory = async () => {
|
|
setLoading(true);
|
|
try {
|
|
// Using generated client to fetch history
|
|
const data = await client.get_workflow_histories({ queries: { limit: 5 } });
|
|
setHistory(data);
|
|
} catch (err) {
|
|
console.error("Failed to fetch history:", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClearHistory = async () => {
|
|
if (confirm("Are you sure you want to clear ALL history? This cannot be undone.")) {
|
|
try {
|
|
const res = await fetch('/api/v1/system/history', { method: 'DELETE' });
|
|
if (res.ok) {
|
|
fetchHistory();
|
|
} else {
|
|
console.error("Failed to clear history");
|
|
alert("Failed to clear history");
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert("Error clearing history");
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchHistory();
|
|
}, []);
|
|
|
|
if (!loading && history.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card className="w-full max-w-4xl mx-auto shadow-md mt-8">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<div className="space-y-1">
|
|
<CardTitle className="text-xl flex items-center gap-2">
|
|
<History className="h-5 w-5" />
|
|
Recent Analysis Reports
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Your recently generated fundamental analysis reports.
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="ghost" size="icon" onClick={handleClearHistory} title="Clear All History">
|
|
<Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" onClick={fetchHistory} disabled={loading}>
|
|
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Symbol</TableHead>
|
|
<TableHead>Market</TableHead>
|
|
<TableHead>Template</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead className="text-right">Action</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading && history.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="h-24 text-center">
|
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
history.map((item) => (
|
|
<TableRow key={item.request_id} className="group cursor-pointer hover:bg-muted/50" onClick={() => navigate(`/history/${item.request_id}`)}>
|
|
<TableCell className="font-medium">{item.symbol}</TableCell>
|
|
<TableCell>{item.market}</TableCell>
|
|
<TableCell className="text-muted-foreground">{templates?.find(t => t.id === item.template_id)?.name || item.template_id || 'Default'}</TableCell>
|
|
<TableCell>
|
|
<StatusBadge status={item.status} />
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{new Date(item.start_time).toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button variant="ghost" size="sm" className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
View <ArrowRight className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StatusBadge({ status }: { status: string }) {
|
|
let variant: "default" | "destructive" | "outline" | "secondary" = "outline";
|
|
let className = "";
|
|
|
|
switch (status.toLowerCase()) {
|
|
case 'completed':
|
|
variant = "default";
|
|
className = "bg-green-600 hover:bg-green-700";
|
|
break;
|
|
case 'failed':
|
|
variant = "destructive";
|
|
break;
|
|
case 'running':
|
|
case 'pending':
|
|
variant = "secondary";
|
|
className = "text-blue-600 bg-blue-100";
|
|
break;
|
|
default:
|
|
variant = "outline";
|
|
}
|
|
|
|
return (
|
|
<Badge variant={variant} className={className}>
|
|
{status}
|
|
</Badge>
|
|
);
|
|
}
|
|
|