Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 88 additions & 16 deletions src/components/CIAnalytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,32 @@ export default function CIAnalytics() {
const [data, setData] = useState<CIAnalyticsData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [rateLimitResetTime, setRateLimitResetTime] = useState<Date | null>(null);
const [isRateLimited, setIsRateLimited] = useState(false);

// Clear rate limit state once reset time passes
useEffect(() => {
if (!rateLimitResetTime) return;

const msUntilReset = rateLimitResetTime.getTime() - Date.now();
if (msUntilReset <= 0) {
setIsRateLimited(false);
setRateLimitResetTime(null);
return;
}

const timer = setTimeout(() => {
setIsRateLimited(false);
setRateLimitResetTime(null);
setError(null);
}, msUntilReset);

return () => clearTimeout(timer);
}, [rateLimitResetTime]);

const fetchCIAnalytics = useCallback(() => {
if (isRateLimited) return;

setLoading(true);
setError(null);

Expand All @@ -28,17 +52,46 @@ export default function CIAnalytics() {

fetch(`/api/metrics/ci${accountParam}`)
.then((res) => {
if (res.status === 403) {
// Read reset time from header
const resetHeader = res.headers.get("X-RateLimit-Reset");
if (resetHeader) {
const resetDate = new Date(parseInt(resetHeader, 10) * 1000);
const resetTimeStr = resetDate.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
setRateLimitResetTime(resetDate);
setIsRateLimited(true);
throw new Error(
`GitHub API rate limit reached. Resets at ${resetTimeStr}. Try again later.`
);
}
throw new Error(
"GitHub API rate limit reached. Please try again later."
);
}

if (!res.ok) {
throw new Error("API error");
}

return res.json();
})
.then((payload: CIAnalyticsData) => setData(payload))
.catch(() =>
setError("CI data unavailable - ensure Actions are enabled on your repos")
)
.then((payload: CIAnalyticsData) => {
setData(payload);
setIsRateLimited(false);
setRateLimitResetTime(null);
})
.catch((err: Error) => {
setError(
err.message.includes("rate limit")
? err.message
: "CI data unavailable - ensure Actions are enabled on your repos"
);
})
.finally(() => setLoading(false));
}, [selectedAccount]);
}, [selectedAccount, isRateLimited]);

useEffect(() => {
fetchCIAnalytics();
Expand All @@ -53,6 +106,15 @@ export default function CIAnalytics() {
]
: [];

const refreshLabel = isRateLimited
? rateLimitResetTime
? `Retry at ${rateLimitResetTime.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`
: "Rate limited"
: "Refresh";

return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<div className="mb-4 flex items-start justify-between gap-3">
Expand All @@ -67,9 +129,11 @@ export default function CIAnalytics() {
<button
type="button"
onClick={fetchCIAnalytics}
className="rounded-md border border-[var(--border)] px-3 py-1.5 text-xs font-medium text-[var(--muted-foreground)] transition-colors hover:bg-[var(--control)]"
disabled={isRateLimited || loading}
title={isRateLimited ? "GitHub API rate limit reached" : "Refresh CI data"}
className="rounded-md border border-[var(--border)] px-3 py-1.5 text-xs font-medium text-[var(--muted-foreground)] transition-colors hover:bg-[var(--control)] disabled:cursor-not-allowed disabled:opacity-50"
>
Refresh
{refreshLabel}
</button>
</div>

Expand All @@ -90,15 +154,23 @@ export default function CIAnalytics() {
))}
</div>
) : error ? (
<div className="rounded-lg border border-red-500/20 bg-red-500/10 p-4 text-sm text-red-400">
<div
className={`rounded-lg border p-4 text-sm ${
isRateLimited
? "border-yellow-500/20 bg-yellow-500/10 text-yellow-400"
: "border-red-500/20 bg-red-500/10 text-red-400"
}`}
>
<p>{error}</p>
<button
type="button"
onClick={fetchCIAnalytics}
className="mt-3 rounded-md border border-red-500/30 px-3 py-1.5 text-xs font-medium text-red-300 transition-colors hover:bg-red-500/10"
>
Try again
</button>
{!isRateLimited && (
<button
type="button"
onClick={fetchCIAnalytics}
className="mt-3 rounded-md border border-red-500/30 px-3 py-1.5 text-xs font-medium text-red-300 transition-colors hover:bg-red-500/10"
>
Try again
</button>
)}
</div>
) : data ? (
<div className="space-y-4">
Expand Down Expand Up @@ -133,4 +205,4 @@ export default function CIAnalytics() {
) : null}
</div>
);
}
}
Loading