Skip to content
Open
Show file tree
Hide file tree
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
44 changes: 44 additions & 0 deletions src/components/github/forms/GitHubTokenInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ interface GitHubTokenInputProps {
onTokenDelete: () => void;
hasSavedToken: boolean;
hasPresetToken: boolean;
rateLimitRemaining: number | null;
rateLimitResetAt: number | null;
rateLimitLoading: boolean;
}

const GitHubTokenInput = ({
Expand All @@ -24,7 +27,38 @@ const GitHubTokenInput = ({
onTokenSave,
onTokenDelete,
hasSavedToken,
rateLimitRemaining,
rateLimitResetAt,
rateLimitLoading,
}: GitHubTokenInputProps) => {
const getResetCountdownText = (): string => {
if (rateLimitResetAt === null) {
return "resets soon";
}

const nowInSeconds = Math.floor(Date.now() / 1000);
const remainingSeconds = Math.max(0, rateLimitResetAt - nowInSeconds);
const remainingMinutes = Math.ceil(remainingSeconds / 60);

return `resets in ${remainingMinutes}m`;
};
Comment on lines +34 to +44

const renderRateLimitCounter = (): string => {
if (!token.trim()) {
return "Hourly calls remaining: —";
}

if (rateLimitLoading) {
return "Hourly calls remaining: loading...";
}

if (rateLimitRemaining === null) {
return "Hourly calls remaining: unavailable";
}

return `Hourly calls remaining: ${rateLimitRemaining} • ${getResetCountdownText()}`;
};

return (
<Box className="mb-5">
<Typography className="form-subtitle" sx={{ cursor: "default" }}>
Expand Down Expand Up @@ -63,6 +97,16 @@ const GitHubTokenInput = ({
</Typography>

<Box sx={{ display: "flex", gap: 3, mt: 2 }}>
<Typography
className="text-xs text-gray-500"
sx={{
display: "flex",
alignItems: "center",
whiteSpace: "nowrap",
}}
>
{renderRateLimitCounter()}
</Typography>
<Button
className="submit-button"
color="primary"
Expand Down
6 changes: 6 additions & 0 deletions src/features/github/analysis/RepoAnalysisForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const RepoAnalysisForm = () => {
tokenMessage,
hasSavedToken,
hasPresetToken,
rateLimitRemaining,
rateLimitResetAt,
rateLimitLoading,
handleTokenChange,
saveToken,
deleteToken,
Expand Down Expand Up @@ -104,6 +107,9 @@ const RepoAnalysisForm = () => {
<GitHubTokenInput
hasPresetToken={hasPresetToken}
hasSavedToken={hasSavedToken}
rateLimitLoading={rateLimitLoading}
rateLimitRemaining={rateLimitRemaining}
rateLimitResetAt={rateLimitResetAt}
token={token}
onTokenChange={handleTokenChange}
onTokenDelete={deleteToken}
Expand Down
56 changes: 56 additions & 0 deletions src/hooks/github/useTokenManagement.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useState, useEffect } from "react";
import type { TokenMessage } from "@/types";
import { fetchRateLimitStatus } from "@/services/github/api";

export interface UseTokenManagementReturn {
token: string;
tokenMessage: TokenMessage | null;
hasSavedToken: boolean;
hasPresetToken: boolean;
rateLimitRemaining: number | null;
rateLimitResetAt: number | null;
rateLimitLoading: boolean;
handleTokenChange: (newToken: string) => void;
saveToken: () => void;
deleteToken: () => void;
Expand All @@ -15,6 +19,11 @@ export interface UseTokenManagementReturn {
export function useTokenManagement(): UseTokenManagementReturn {
const [token, setToken] = useState<string>("");
const [tokenMessage, setTokenMessage] = useState<TokenMessage | null>(null);
const [rateLimitRemaining, setRateLimitRemaining] = useState<number | null>(
null
);
const [rateLimitResetAt, setRateLimitResetAt] = useState<number | null>(null);
const [rateLimitLoading, setRateLimitLoading] = useState<boolean>(false);

// Get the GitHub token from localStorage first, then fallback to environment variables
useEffect(() => {
Expand All @@ -29,6 +38,50 @@ export function useTokenManagement(): UseTokenManagementReturn {
}
}, []);

useEffect(() => {
let isCancelled = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;

const loadRateLimit = async (): Promise<void> => {
if (!token.trim()) {
setRateLimitRemaining(null);
setRateLimitResetAt(null);
setRateLimitLoading(false);
return;
}

setRateLimitLoading(true);

try {
const { remaining, resetAt } = await fetchRateLimitStatus(token.trim());
if (!isCancelled) {
setRateLimitRemaining(remaining);
setRateLimitResetAt(resetAt);
}
} catch {
if (!isCancelled) {
setRateLimitRemaining(null);
setRateLimitResetAt(null);
}
} finally {
if (!isCancelled) {
setRateLimitLoading(false);
}
}
};

timeoutId = setTimeout(() => {
void loadRateLimit();
}, 400);

return () => {
isCancelled = true;
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [token]);
Comment on lines +41 to +83

// Check if there's a saved token in localStorage
const hasSavedToken = !!localStorage.getItem("githubToken");

Expand Down Expand Up @@ -81,6 +134,9 @@ export function useTokenManagement(): UseTokenManagementReturn {
tokenMessage,
hasSavedToken,
hasPresetToken,
rateLimitRemaining,
rateLimitResetAt,
rateLimitLoading,
handleTokenChange,
saveToken,
deleteToken,
Expand Down
43 changes: 42 additions & 1 deletion src/services/github/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import axios from 'axios';

interface GitHubRateLimitResponse {
resources?: {
core?: {
remaining?: number;
reset?: number;
};
};
}
Comment on lines +3 to +10

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix this.


export interface RateLimitStatus {
remaining: number;
resetAt: number | null;
}

/**
* Send a GraphQL request to GitHub API
* @param query GraphQL query
Expand Down Expand Up @@ -32,4 +46,31 @@ export const graphqlRequest = async (
console.error('GraphQL API request failed:', error);
throw new Error(`GitHub API request failed: ${(error as Error).message}`);
}
};
};

/**
* Fetch remaining REST API calls for the current hour using GitHub Rate Limit API
* @param token GitHub access token
* @returns Remaining API calls and reset timestamp in the hourly budget
*/
export const fetchRateLimitStatus = async (token: string): Promise<RateLimitStatus> => {
try {
const response = await axios.get<GitHubRateLimitResponse>(
'https://api.github.com/rate_limit',
{
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
},
}
);

return {
remaining: response.data.resources?.core?.remaining ?? 0,
resetAt: response.data.resources?.core?.reset ?? null,
};
} catch (error) {
console.error('Rate limit API request failed:', error);
throw new Error(`Failed to fetch GitHub rate limit: ${(error as Error).message}`);
}
};