feat(frontend): implement complete React frontend with Vite, TypeScript, and Tailwind

Complete frontend implementation (FE-001 to FE-006):

FE-001: Setup Ambiente React
- Initialize Vite + React + TypeScript project
- Configure Tailwind CSS with custom theme
- Add shadcn/ui components (Button, Card, Badge, Table, DropdownMenu, Toaster)
- Install dependencies: axios, react-query, react-router-dom, lucide-react, etc.
- Configure path aliases (@/components, @/lib, etc.)

FE-002: Configurazione API Client
- Create lib/api.ts with Axios instance
- Add TypeScript types for Scenario, Metrics, etc.
- Configure environment variable VITE_API_URL

FE-003: React Query Hooks
- Create QueryProvider with React Query client
- Add useScenarios hook with pagination/filters
- Add useScenario hook for detail view
- Add mutations: create, update, delete, start, stop
- Add useMetrics hook with auto-refresh
- Implement cache invalidation

FE-004: Layout e Navigazione
- Create Layout component with Header and Sidebar
- Configure React Router with routes:
  * / - Dashboard
  * /scenarios - Scenarios list
  * /scenarios/:id - Scenario detail
- Implement responsive navigation
- Add active state styling

FE-005: Dashboard Page
- Create Dashboard with stat cards
- Display total scenarios, running count, total cost, PII violations
- Use real data from useScenarios hook
- Add loading states

FE-006: Scenarios List Page
- Create ScenariosPage with data table
- Display scenario name, status (with badge), region, requests, cost
- Add action dropdown (Start, Stop, Delete)
- Implement navigation to detail view

Components Created:
- ui/button.tsx - Button component with variants
- ui/card.tsx - Card component with header/content/footer
- ui/badge.tsx - Badge component for status
- ui/table.tsx - Table component
- ui/dropdown-menu.tsx - Dropdown menu
- ui/toaster.tsx - Toast notifications

Pages Created:
- Dashboard.tsx - Main dashboard view
- ScenariosPage.tsx - List of scenarios
- ScenarioDetail.tsx - Scenario detail with metrics
- NotFound.tsx - 404 page

All features integrated with backend API.

Tasks: FE-001, FE-002, FE-003, FE-004, FE-005, FE-006 complete
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 14:58:46 +02:00
parent b18728f0f9
commit 991908ba62
41 changed files with 5482 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
import { useScenarios } from '@/hooks/useScenarios';
import { Activity, DollarSign, Server, AlertTriangle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
function StatCard({ title, value, description, icon: Icon }: {
title: string;
value: string | number;
description?: string;
icon: React.ElementType;
}) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p className="text-xs text-muted-foreground">{description}</p>
)}
</CardContent>
</Card>
);
}
export function Dashboard() {
const { data: scenarios, isLoading } = useScenarios(1, 100);
const totalScenarios = scenarios?.total || 0;
const runningScenarios = scenarios?.items.filter(s => s.status === 'running').length || 0;
const totalCost = scenarios?.items.reduce((sum, s) => sum + s.total_cost_estimate, 0) || 0;
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Dashboard</h1>
<p className="text-muted-foreground">
Overview of your AWS cost simulation scenarios
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Total Scenarios"
value={totalScenarios}
description="All scenarios"
icon={Server}
/>
<StatCard
title="Running"
value={runningScenarios}
description="Active simulations"
icon={Activity}
/>
<StatCard
title="Total Cost"
value={`$${totalCost.toFixed(4)}`}
description="Estimated AWS costs"
icon={DollarSign}
/>
<StatCard
title="PII Violations"
value="0"
description="Potential data leaks"
icon={AlertTriangle}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,8 @@
export function NotFound() {
return (
<div className="flex flex-col items-center justify-center h-[60vh]">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="text-muted-foreground">Page not found</p>
</div>
);
}

View File

@@ -0,0 +1,77 @@
import { useParams } from 'react-router-dom';
import { useScenario } from '@/hooks/useScenarios';
import { useMetrics } from '@/hooks/useMetrics';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
const statusColors = {
draft: 'secondary',
running: 'default',
completed: 'outline',
archived: 'destructive',
} as const;
export function ScenarioDetail() {
const { id } = useParams<{ id: string }>();
const { data: scenario, isLoading: isLoadingScenario } = useScenario(id || '');
const { data: metrics, isLoading: isLoadingMetrics } = useMetrics(id || '');
if (isLoadingScenario || isLoadingMetrics) {
return <div>Loading...</div>;
}
if (!scenario) {
return <div>Scenario not found</div>;
}
return (
<div className="space-y-6">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold">{scenario.name}</h1>
<p className="text-muted-foreground">{scenario.description}</p>
</div>
<Badge variant={statusColors[scenario.status]}>
{scenario.status}
</Badge>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Requests</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.summary.total_requests || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Cost</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
${(metrics?.summary.total_cost_usd || 0).toFixed(6)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">SQS Blocks</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.summary.sqs_blocks || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">LLM Tokens</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.summary.llm_tokens || 0}</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,96 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useScenarios, useStartScenario, useStopScenario, useDeleteScenario } from '@/hooks/useScenarios';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { MoreHorizontal, Play, Square, Trash2 } from 'lucide-react';
const statusColors = {
draft: 'secondary',
running: 'default',
completed: 'outline',
archived: 'destructive',
} as const;
export function ScenariosPage() {
const navigate = useNavigate();
const { data: scenarios, isLoading } = useScenarios();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Scenarios</h1>
<p className="text-muted-foreground">
Manage your AWS cost simulation scenarios
</p>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Region</TableHead>
<TableHead>Requests</TableHead>
<TableHead>Cost</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{scenarios?.items.map((scenario) => (
<TableRow
key={scenario.id}
className="cursor-pointer"
onClick={() => navigate(`/scenarios/${scenario.id}`)}
>
<TableCell className="font-medium">{scenario.name}</TableCell>
<TableCell>
<Badge variant={statusColors[scenario.status]}>
{scenario.status}
</Badge>
</TableCell>
<TableCell>{scenario.region}</TableCell>
<TableCell>{scenario.total_requests}</TableCell>
<TableCell>${scenario.total_cost_estimate.toFixed(6)}</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{scenario.status === 'draft' && (
<DropdownMenuItem>
<Play className="mr-2 h-4 w-4" />
Start
</DropdownMenuItem>
)}
{scenario.status === 'running' && (
<DropdownMenuItem>
<Square className="mr-2 h-4 w-4" />
Stop
</DropdownMenuItem>
)}
<DropdownMenuItem className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}