Files
mockupAWS/frontend/src/components/charts/CostBreakdown.tsx
Luca Sacchi Ricciardi d222d21618 docs: update documentation for v0.4.0 release
- Update README.md with v0.4.0 features and screenshots placeholders
- Update architecture.md with v0.4.0 implementation status
- Update progress.md marking all 27 tasks as completed
- Create CHANGELOG.md with complete release notes
- Add v0.4.0 frontend components and hooks
2026-04-07 18:07:23 +02:00

145 lines
4.4 KiB
TypeScript

import { useState } from 'react';
import {
PieChart,
Pie,
Cell,
ResponsiveContainer,
Tooltip,
} from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { CostBreakdown as CostBreakdownType } from '@/types/api';
import { CHART_COLORS, formatCurrency } from './chart-utils';
interface CostBreakdownChartProps {
data: CostBreakdownType[];
title?: string;
description?: string;
}
// Map services to colors
const SERVICE_COLORS: Record<string, string> = {
sqs: CHART_COLORS.sqs,
lambda: CHART_COLORS.lambda,
bedrock: CHART_COLORS.bedrock,
s3: CHART_COLORS.blue,
cloudwatch: CHART_COLORS.green,
default: CHART_COLORS.secondary,
};
function getServiceColor(service: string): string {
const normalized = service.toLowerCase().replace(/[^a-z]/g, '');
return SERVICE_COLORS[normalized] || SERVICE_COLORS.default;
}
// Tooltip component defined outside main component
interface CostTooltipProps {
active?: boolean;
payload?: Array<{ payload: CostBreakdownType }>;
}
function CostTooltip({ active, payload }: CostTooltipProps) {
if (active && payload && payload.length) {
const item = payload[0].payload;
return (
<div className="rounded-lg border bg-popover p-3 shadow-md">
<p className="font-medium text-popover-foreground">{item.service}</p>
<p className="text-sm text-muted-foreground">
Cost: {formatCurrency(item.cost_usd)}
</p>
<p className="text-sm text-muted-foreground">
Percentage: {item.percentage.toFixed(1)}%
</p>
</div>
);
}
return null;
}
export function CostBreakdownChart({
data,
title = 'Cost Breakdown',
description = 'Cost distribution by service',
}: CostBreakdownChartProps) {
const [hiddenServices, setHiddenServices] = useState<Set<string>>(new Set());
const filteredData = data.filter((item) => !hiddenServices.has(item.service));
const toggleService = (service: string) => {
setHiddenServices((prev) => {
const next = new Set(prev);
if (next.has(service)) {
next.delete(service);
} else {
next.add(service);
}
return next;
});
};
const totalCost = filteredData.reduce((sum, item) => sum + item.cost_usd, 0);
return (
<Card className="w-full">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
<p className="text-2xl font-bold mt-2">{formatCurrency(totalCost)}</p>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={filteredData}
cx="50%"
cy="45%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
dataKey="cost_usd"
nameKey="service"
animationBegin={0}
animationDuration={800}
>
{filteredData.map((entry) => (
<Cell
key={`cell-${entry.service}`}
fill={getServiceColor(entry.service)}
stroke="hsl(var(--card))"
strokeWidth={2}
/>
))}
</Pie>
<Tooltip content={<CostTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex flex-wrap justify-center gap-4 mt-4">
{data.map((item) => {
const isHidden = hiddenServices.has(item.service);
return (
<button
key={item.service}
onClick={() => toggleService(item.service)}
className={`flex items-center gap-2 text-sm transition-opacity hover:opacity-80 ${
isHidden ? 'opacity-40' : 'opacity-100'
}`}
>
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: getServiceColor(item.service) }}
/>
<span className="text-muted-foreground">
{item.service} ({item.percentage.toFixed(1)}%)
</span>
</button>
);
})}
</div>
</CardContent>
</Card>
);
}