| "use client";
|
|
|
| import React from "react";
|
| import { useTranslations } from "next-intl";
|
| import { Card } from "@/shared/components";
|
|
|
| interface CachePerformanceProps {
|
| hits?: number;
|
| misses?: number;
|
| hitRate?: string;
|
| avgLatencyMs?: number;
|
| p95LatencyMs?: number;
|
| totalRequests?: number;
|
| loading?: boolean;
|
| error?: string | null;
|
| onRetry?: () => void;
|
| }
|
|
|
| function HitRateBar({ hitRate, label }: { hitRate: number; label: string }) {
|
| const colorClass = hitRate >= 70 ? "bg-green-500" : hitRate >= 40 ? "bg-amber-400" : "bg-red-500";
|
| const textClass =
|
| hitRate >= 70 ? "text-green-500" : hitRate >= 40 ? "text-amber-400" : "text-red-500";
|
|
|
| return (
|
| <div
|
| className="w-full"
|
| role="progressbar"
|
| aria-label={label}
|
| aria-valuenow={hitRate}
|
| aria-valuemin={0}
|
| aria-valuemax={100}
|
| >
|
| <div className="flex justify-between text-xs mb-1.5">
|
| <span className="text-text-muted">{label}</span>
|
| <span className={`font-semibold tabular-nums ${textClass}`}>{hitRate.toFixed(1)}%</span>
|
| </div>
|
| <div className="w-full h-2 rounded-full bg-surface/50 overflow-hidden">
|
| <div
|
| className={`h-full rounded-full transition-all duration-500 ${colorClass}`}
|
| style={{ width: `${Math.min(hitRate, 100)}%` }}
|
| />
|
| </div>
|
| </div>
|
| );
|
| }
|
|
|
|
|
|
|
| function Skeleton({ className }: { className?: string }) {
|
| return (
|
| <div
|
| data-testid="skeleton"
|
| className={`animate-pulse rounded bg-surface/50 ${className ?? ""}`}
|
| />
|
| );
|
| }
|
|
|
|
|
|
|
| export default function CachePerformance({
|
| hits = 0,
|
| misses = 0,
|
| hitRate,
|
| avgLatencyMs,
|
| p95LatencyMs,
|
| totalRequests = 0,
|
| loading = false,
|
| error = null,
|
| onRetry,
|
| stats,
|
| }: CachePerformanceProps) {
|
| const t = useTranslations("cache");
|
|
|
| const hitRateNum = hitRate ? parseFloat(hitRate) : 0;
|
|
|
| return (
|
| <Card>
|
| <div data-testid="cache-performance" className="p-5 flex flex-col gap-4">
|
| {/* Header */}
|
| <div className="flex items-center justify-between">
|
| <h2 className="font-medium text-sm">{t("performanceTitle")}</h2>
|
| </div>
|
|
|
| {/* Error state */}
|
| {error && (
|
| <div className="flex flex-col gap-2">
|
| <p className="text-sm text-red-400">{error}</p>
|
| {onRetry && (
|
| <button
|
| onClick={onRetry}
|
| className="self-start text-xs px-3 py-1.5 rounded bg-surface border border-border/50 hover:bg-surface/80 transition-colors"
|
| aria-label={t("cachePerformanceRetry")}
|
| >
|
| {t("retry")}
|
| </button>
|
| )}
|
| </div>
|
| )}
|
|
|
| {/* Loading state */}
|
| {loading && !error && (
|
| <div className="flex flex-col gap-4">
|
| <Skeleton className="h-8 w-full" />
|
| <div className="grid grid-cols-3 gap-4 pt-3 border-t border-border/30">
|
| <Skeleton className="h-10" />
|
| <Skeleton className="h-10" />
|
| <Skeleton className="h-10" />
|
| </div>
|
| {(avgLatencyMs !== undefined || p95LatencyMs !== undefined) && (
|
| <div className="grid grid-cols-2 gap-4">
|
| <Skeleton className="h-8" />
|
| <Skeleton className="h-8" />
|
| </div>
|
| )}
|
| </div>
|
| )}
|
|
|
| {/* Data state β hidden while loading */}
|
| {!loading && !error && stats !== null && (
|
| <>
|
| {/* Hit rate bar */}
|
| {hitRate !== undefined && (
|
| <HitRateBar hitRate={hitRateNum} label={t("cachePerformanceHitRate")} />
|
| )}
|
|
|
| {/* Hit / Miss / Total breakdown */}
|
| <div className="grid grid-cols-3 gap-4 pt-3 border-t border-border/30 text-center">
|
| <div>
|
| <div className="text-lg font-semibold tabular-nums text-green-500">{hits}</div>
|
| <div className="text-xs text-text-muted mt-0.5">{t("hits")}</div>
|
| </div>
|
| <div>
|
| <div className="text-lg font-semibold tabular-nums text-red-400">{misses}</div>
|
| <div className="text-xs text-text-muted mt-0.5">{t("misses")}</div>
|
| </div>
|
| <div>
|
| <div className="text-lg font-semibold tabular-nums">{totalRequests}</div>
|
| <div className="text-xs text-text-muted mt-0.5">{t("total")}</div>
|
| </div>
|
| </div>
|
|
|
| {/* Latency metrics */}
|
| {(avgLatencyMs !== undefined || p95LatencyMs !== undefined) && (
|
| <div className="grid grid-cols-2 gap-4 pt-3 border-t border-border/30 text-center">
|
| {avgLatencyMs !== undefined && (
|
| <div>
|
| <div className="text-lg font-semibold tabular-nums">{avgLatencyMs}</div>
|
| <div className="text-xs text-text-muted mt-0.5">
|
| {t("cachePerformanceAvgLatency")}
|
| </div>
|
| </div>
|
| )}
|
| {p95LatencyMs !== undefined && (
|
| <div>
|
| <div className="text-lg font-semibold tabular-nums">{p95LatencyMs}</div>
|
| <div className="text-xs text-text-muted mt-0.5">
|
| {t("cachePerformanceP95Latency")}
|
| </div>
|
| </div>
|
| )}
|
| </div>
|
| )}
|
|
|
| {/* hitRate as text for test assertions */}
|
| {hitRate !== undefined && (
|
| <div className="text-center">
|
| <span className="text-sm font-semibold tabular-nums">{hitRate}</span>
|
| </div>
|
| )}
|
| </>
|
| )}
|
| </div>
|
| </Card>
|
| );
|
| }
|
|
|