Fundamental_Analysis/frontend/src/components/RecentWorkflowsList.tsx
Lv, Qi abe47c4bc8 refactor(report): switch to HTML+Gotenberg for high-quality PDF export
- 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
2025-11-30 22:43:22 +08:00

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>
);
}