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
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { format } from 'date-fns';
|
||||
import { formatCurrency, formatNumber } from './ChartContainer';
|
||||
|
||||
interface TimeSeriesDataPoint {
|
||||
timestamp: string;
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
interface TimeSeriesChartProps {
|
||||
data: TimeSeriesDataPoint[];
|
||||
series: Array<{
|
||||
key: string;
|
||||
name: string;
|
||||
color: string;
|
||||
type?: 'line' | 'area';
|
||||
}>;
|
||||
title?: string;
|
||||
description?: string;
|
||||
yAxisFormatter?: (value: number) => string;
|
||||
chartType?: 'line' | 'area';
|
||||
}
|
||||
|
||||
export function TimeSeriesChart({
|
||||
data,
|
||||
series,
|
||||
title = 'Metrics Over Time',
|
||||
description,
|
||||
yAxisFormatter = formatNumber,
|
||||
chartType = 'area',
|
||||
}: TimeSeriesChartProps) {
|
||||
const formatXAxis = (timestamp: string) => {
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return format(date, 'MMM dd HH:mm');
|
||||
} catch {
|
||||
return timestamp;
|
||||
}
|
||||
};
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: {
|
||||
active?: boolean;
|
||||
payload?: Array<{ name: string; value: number; color: string }>;
|
||||
label?: string;
|
||||
}) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-popover p-3 shadow-md">
|
||||
<p className="font-medium text-popover-foreground mb-2">
|
||||
{label ? formatXAxis(label) : ''}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{payload.map((entry) => (
|
||||
<p key={entry.name} className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<span
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
{entry.name}: {yAxisFormatter(entry.value)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const ChartComponent = chartType === 'area' ? AreaChart : LineChart;
|
||||
|
||||
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>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[350px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ChartComponent
|
||||
data={data}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
{series.map((s) => (
|
||||
<linearGradient
|
||||
key={s.key}
|
||||
id={`gradient-${s.key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="5%" stopColor={s.color} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={s.color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="hsl(var(--border))"
|
||||
opacity={0.3}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={formatXAxis}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={30}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={yAxisFormatter}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
wrapperStyle={{ paddingTop: '20px' }}
|
||||
iconType="circle"
|
||||
/>
|
||||
{series.map((s) =>
|
||||
chartType === 'area' ? (
|
||||
<Area
|
||||
key={s.key}
|
||||
type="monotone"
|
||||
dataKey={s.key}
|
||||
name={s.name}
|
||||
stroke={s.color}
|
||||
fill={`url(#gradient-${s.key})`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<Line
|
||||
key={s.key}
|
||||
type="monotone"
|
||||
dataKey={s.key}
|
||||
name={s.name}
|
||||
stroke={s.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ChartComponent>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-configured chart for cost metrics
|
||||
export function CostTimeSeriesChart({
|
||||
data,
|
||||
title = 'Cost Over Time',
|
||||
description = 'Cumulative costs by service',
|
||||
}: {
|
||||
data: TimeSeriesDataPoint[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const series = [
|
||||
{ key: 'sqs_cost', name: 'SQS', color: '#FF9900', type: 'area' as const },
|
||||
{ key: 'lambda_cost', name: 'Lambda', color: '#F97316', type: 'area' as const },
|
||||
{ key: 'bedrock_cost', name: 'Bedrock', color: '#8B5CF6', type: 'area' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<TimeSeriesChart
|
||||
data={data}
|
||||
series={series}
|
||||
title={title}
|
||||
description={description}
|
||||
yAxisFormatter={formatCurrency}
|
||||
chartType="area"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-configured chart for request metrics
|
||||
export function RequestTimeSeriesChart({
|
||||
data,
|
||||
title = 'Requests Over Time',
|
||||
description = 'Request volume trends',
|
||||
}: {
|
||||
data: TimeSeriesDataPoint[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const series = [
|
||||
{ key: 'requests', name: 'Requests', color: '#3B82F6', type: 'line' as const },
|
||||
{ key: 'errors', name: 'Errors', color: '#EF4444', type: 'line' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<TimeSeriesChart
|
||||
data={data}
|
||||
series={series}
|
||||
title={title}
|
||||
description={description}
|
||||
yAxisFormatter={formatNumber}
|
||||
chartType="line"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user