285a748d6a
- Change generic 'frontend' title to 'mockupAWS - AWS Cost Simulator' - Resolves frontend branding issue identified in testing
235 lines
6.6 KiB
TypeScript
235 lines
6.6 KiB
TypeScript
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 './chart-utils';
|
|
|
|
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';
|
|
}
|
|
|
|
// Format timestamp for display
|
|
function formatXAxisLabel(timestamp: string): string {
|
|
try {
|
|
const date = new Date(timestamp);
|
|
return format(date, 'MMM dd HH:mm');
|
|
} catch {
|
|
return timestamp;
|
|
}
|
|
}
|
|
|
|
// Tooltip component defined outside main component
|
|
interface TimeTooltipProps {
|
|
active?: boolean;
|
|
payload?: Array<{ name: string; value: number; color: string }>;
|
|
label?: string;
|
|
yAxisFormatter?: (value: number) => string;
|
|
}
|
|
|
|
function TimeTooltip({ active, payload, label, yAxisFormatter }: TimeTooltipProps) {
|
|
if (active && payload && payload.length && yAxisFormatter) {
|
|
return (
|
|
<div className="rounded-lg border bg-popover p-3 shadow-md">
|
|
<p className="font-medium text-popover-foreground mb-2">
|
|
{label ? formatXAxisLabel(label) : ''}
|
|
</p>
|
|
<div className="space-y-1">
|
|
{payload.map((entry: { name: string; value: number; color: string }) => (
|
|
<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;
|
|
}
|
|
|
|
export function TimeSeriesChart({
|
|
data,
|
|
series,
|
|
title = 'Metrics Over Time',
|
|
description,
|
|
yAxisFormatter = formatNumber,
|
|
chartType = 'area',
|
|
}: TimeSeriesChartProps) {
|
|
const formatXAxis = (timestamp: string) => formatXAxisLabel(timestamp);
|
|
|
|
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={<TimeTooltip yAxisFormatter={yAxisFormatter} />} />
|
|
<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"
|
|
/>
|
|
);
|
|
}
|