Files
mockupAWS/frontend/src/pages/ScenarioDetail.tsx
Luca Sacchi Ricciardi a5fc85897b
Some checks failed
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled
feat: implement v0.4.0 - Reports, Charts, Comparison, Dark Mode, E2E Testing
Backend (@backend-dev):
- Add ReportService with PDF/CSV generation (reportlab, pandas)
- Implement Report API endpoints (POST, GET, DELETE, download)
- Add ReportRepository and schemas
- Configure storage with auto-cleanup (30 days)
- Rate limiting: 10 downloads/minute
- Professional PDF templates with charts support

Frontend (@frontend-dev):
- Integrate Recharts for data visualization
- Add CostBreakdown, TimeSeries, ComparisonBar charts
- Implement scenario comparison page with multi-select
- Add dark/light mode toggle with ThemeProvider
- Create Reports page with generation form and list
- Add new UI components: checkbox, dialog, tabs, label, skeleton
- Implement useComparison and useReports hooks

QA (@qa-engineer):
- Setup Playwright E2E testing framework
- Create 7 test spec files with 94 test cases
- Add visual regression testing with baselines
- Configure multi-browser testing (Chrome, Firefox, WebKit)
- Add mobile responsive tests
- Create test fixtures and helpers
- Setup GitHub Actions CI workflow

Documentation (@spec-architect):
- Create detailed kanban-v0.4.0.md with 27 tasks
- Update progress.md with v0.4.0 tracking
- Create v0.4.0 planning prompt

Features:
 PDF/CSV Report Generation
 Interactive Charts (Pie, Area, Bar)
 Scenario Comparison (2-4 scenarios)
 Dark/Light Mode Toggle
 E2E Test Suite (94 tests)

Dependencies added:
- Backend: reportlab, pandas, slowapi
- Frontend: recharts, date-fns, @radix-ui/react-checkbox/dialog/tabs
- Testing: @playwright/test

27 tasks completed, 100% v0.4.0 implementation
2026-04-07 16:11:47 +02:00

303 lines
11 KiB
TypeScript

import { useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { FileText, ArrowLeft, Play, Square, BarChart3, PieChart, Activity } from 'lucide-react';
import { useScenario, useStartScenario, useStopScenario } from '@/hooks/useScenarios';
import { useMetrics } from '@/hooks/useMetrics';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CostBreakdownChart, TimeSeriesChart } from '@/components/charts';
import { formatCurrency, formatNumber } from '@/components/charts/ChartContainer';
import { Skeleton } from '@/components/ui/skeleton';
const statusColors = {
draft: 'secondary',
running: 'default',
completed: 'outline',
archived: 'destructive',
} as const;
interface TimeSeriesDataPoint {
timestamp: string;
[key: string]: string | number;
}
export function ScenarioDetail() {
const { id } = useParams<{ id: string }>();
const { data: scenario, isLoading: isLoadingScenario } = useScenario(id || '');
const { data: metrics, isLoading: isLoadingMetrics } = useMetrics(id || '');
const [activeTab, setActiveTab] = useState('overview');
const startScenario = useStartScenario(id || '');
const stopScenario = useStopScenario(id || '');
const handleStart = () => startScenario.mutate();
const handleStop = () => stopScenario.mutate();
if (isLoadingScenario || isLoadingMetrics) {
return (
<div className="space-y-6">
<Skeleton className="h-10 w-64" />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-32" />
))}
</div>
<Skeleton className="h-[400px]" />
</div>
);
}
if (!scenario) {
return (
<div className="flex flex-col items-center justify-center h-[60vh]">
<p className="text-muted-foreground mb-4">Scenario not found</p>
<Link to="/scenarios">
<Button>Back to Scenarios</Button>
</Link>
</div>
);
}
// Prepare time series data
const timeseriesData = metrics?.timeseries?.map((point) => ({
timestamp: point.timestamp,
value: point.value,
metric_type: point.metric_type,
})) || [];
// Group time series by metric type
const groupedTimeseries = timeseriesData.reduce((acc, point) => {
if (!acc[point.metric_type]) {
acc[point.metric_type] = [];
}
acc[point.metric_type].push(point);
return acc;
}, {} as Record<string, typeof timeseriesData>);
// Transform for chart
const chartData: TimeSeriesDataPoint[] = Object.keys(groupedTimeseries).length > 0
? groupedTimeseries[Object.keys(groupedTimeseries)[0]].map((point, index) => {
const dataPoint: TimeSeriesDataPoint = {
timestamp: point.timestamp,
};
Object.keys(groupedTimeseries).forEach((type) => {
const typeData = groupedTimeseries[type];
dataPoint[type] = typeData[index]?.value || 0;
});
return dataPoint;
})
: [];
const timeSeriesSeries = Object.keys(groupedTimeseries).map((type, index) => ({
key: type,
name: type.replace(/_/g, ' ').toUpperCase(),
color: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'][index % 5],
type: 'line' as const,
}));
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<Link to="/scenarios">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div>
<div className="flex items-center gap-3">
<h1 className="text-3xl font-bold">{scenario.name}</h1>
<Badge variant={statusColors[scenario.status]}>
{scenario.status}
</Badge>
</div>
<p className="text-muted-foreground mt-1">{scenario.description}</p>
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
<span>Region: {scenario.region}</span>
<span></span>
<span>Created: {new Date(scenario.created_at).toLocaleDateString()}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Link to={`/scenarios/${id}/reports`}>
<Button variant="outline">
<FileText className="mr-2 h-4 w-4" />
Reports
</Button>
</Link>
{scenario.status === 'draft' && (
<Button onClick={handleStart} disabled={startScenario.isPending}>
<Play className="mr-2 h-4 w-4" />
Start
</Button>
)}
{scenario.status === 'running' && (
<Button onClick={handleStop} disabled={stopScenario.isPending} variant="secondary">
<Square className="mr-2 h-4 w-4" />
Stop
</Button>
)}
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Requests</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(metrics?.summary.total_requests || 0)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Cost</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatCurrency(metrics?.summary.total_cost_usd || 0)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">SQS Blocks</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(metrics?.summary.sqs_blocks || 0)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Lambda Invocations</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(metrics?.summary.lambda_invocations || 0)}
</div>
</CardContent>
</Card>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="overview">
<PieChart className="mr-2 h-4 w-4" />
Overview
</TabsTrigger>
<TabsTrigger value="metrics">
<BarChart3 className="mr-2 h-4 w-4" />
Metrics
</TabsTrigger>
<TabsTrigger value="analysis">
<Activity className="mr-2 h-4 w-4" />
Analysis
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
<div className="grid gap-6 lg:grid-cols-2">
{/* Cost Breakdown Chart */}
{metrics?.cost_breakdown && metrics.cost_breakdown.length > 0 && (
<CostBreakdownChart
data={metrics.cost_breakdown}
title="Cost by Service"
description="Distribution of costs across AWS services"
/>
)}
{/* Summary Card */}
<Card>
<CardHeader>
<CardTitle>Additional Metrics</CardTitle>
<CardDescription>Detailed breakdown of scenario metrics</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center py-2 border-b">
<span className="text-muted-foreground">LLM Tokens</span>
<span className="font-medium">{formatNumber(metrics?.summary.llm_tokens || 0)}</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-muted-foreground">PII Violations</span>
<span className="font-medium">{formatNumber(metrics?.summary.pii_violations || 0)}</span>
</div>
<div className="flex justify-between items-center py-2 border-b">
<span className="text-muted-foreground">Avg Cost per Request</span>
<span className="font-medium">
{metrics?.summary.total_requests
? formatCurrency(metrics.summary.total_cost_usd / metrics.summary.total_requests)
: '$0.0000'}
</span>
</div>
<div className="flex justify-between items-center py-2">
<span className="text-muted-foreground">Status</span>
<Badge variant={statusColors[scenario.status]}>{scenario.status}</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="metrics" className="space-y-6">
{chartData.length > 0 ? (
<TimeSeriesChart
data={chartData}
series={timeSeriesSeries}
title="Metrics Over Time"
description="Track metric trends throughout the scenario execution"
chartType="line"
/>
) : (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
No time series data available yet
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="analysis" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Analysis</CardTitle>
<CardDescription>Advanced analysis and insights</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="rounded-lg bg-muted p-4">
<p className="font-medium mb-2">Cost Efficiency</p>
<p className="text-sm text-muted-foreground">
{metrics?.summary.total_requests
? `Average cost per request: ${formatCurrency(
metrics.summary.total_cost_usd / metrics.summary.total_requests
)}`
: 'No request data available'}
</p>
</div>
<div className="rounded-lg bg-muted p-4">
<p className="font-medium mb-2">PII Risk Assessment</p>
<p className="text-sm text-muted-foreground">
{metrics?.summary.pii_violations
? `${metrics.summary.pii_violations} potential PII violations detected`
: 'No PII violations detected'}
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}