Skip to content

Commit 05dce4d

Browse files
feat: Implement GitHub API rate limit status component and related services
1 parent 8220e85 commit 05dce4d

11 files changed

Lines changed: 558 additions & 0 deletions

File tree

‎src-tauri/src/auth.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ pub async fn get_auth_status(app: tauri::AppHandle) -> Result<AuthStatus, String
8282
}
8383
}
8484

85+
#[tauri::command]
86+
pub async fn get_token(app: tauri::AppHandle) -> Result<Option<String>, String> {
87+
let state = app.try_state::<AppState>().ok_or("No state")?;
88+
let config = state.config.lock().unwrap();
89+
Ok(config.access_token.clone())
90+
}
91+
8592
#[tauri::command]
8693
pub async fn logout(app: tauri::AppHandle) -> Result<(), String> {
8794
if let Some(state) = app.try_state::<AppState>() {

‎src-tauri/src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ fn main() {
164164
.invoke_handler(tauri::generate_handler![
165165
auth::set_token,
166166
auth::get_auth_status,
167+
auth::get_token,
167168
auth::logout,
168169
repos::get_owners,
169170
repos::get_repos_for_owner,
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { Injectable } from '@angular/core';
2+
import { TauriService } from './tauri/tauri.service';
3+
4+
export interface RateLimit {
5+
limit: number;
6+
remaining: number;
7+
resetAt: string | number;
8+
used: number;
9+
}
10+
11+
export interface GraphQLRateLimit {
12+
limit: number;
13+
remaining: number;
14+
resetAt: string | number;
15+
cost: number;
16+
}
17+
18+
export interface GitHubRateLimits {
19+
core: RateLimit;
20+
search: RateLimit;
21+
graphql: GraphQLRateLimit;
22+
}
23+
24+
@Injectable({
25+
providedIn: 'root',
26+
})
27+
export class GitHubRateLimitService {
28+
private readonly API_BASE_URL = 'https://api.github.com';
29+
30+
constructor(private tauriService: TauriService) {}
31+
32+
/**
33+
* Get GitHub API rate limits
34+
*/
35+
async getRateLimits(): Promise<GitHubRateLimits | null> {
36+
try {
37+
const authStatus = await this.tauriService.getAuthStatus();
38+
if (!authStatus.authenticated) {
39+
return null;
40+
}
41+
42+
// Get rate limits from REST API
43+
const restResponse = await this.makeAuthenticatedRequest(
44+
`${this.API_BASE_URL}/rate_limit`
45+
);
46+
47+
// Get GraphQL rate limits
48+
const graphqlResponse = await this.makeGraphQLRequest();
49+
50+
// Debug logging
51+
console.log('REST API Response:', restResponse);
52+
console.log('GraphQL Response:', graphqlResponse);
53+
54+
return {
55+
core: {
56+
limit: restResponse.rate.limit,
57+
remaining: restResponse.rate.remaining,
58+
resetAt: restResponse.rate.reset,
59+
used:
60+
restResponse.rate.used ||
61+
restResponse.rate.limit - restResponse.rate.remaining,
62+
},
63+
search: {
64+
limit: restResponse.resources.search.limit,
65+
remaining: restResponse.resources.search.remaining,
66+
resetAt: restResponse.resources.search.reset,
67+
used:
68+
restResponse.resources.search.used ||
69+
restResponse.resources.search.limit -
70+
restResponse.resources.search.remaining,
71+
},
72+
graphql: graphqlResponse.rateLimit,
73+
};
74+
} catch (error) {
75+
console.error('Error fetching rate limits:', error);
76+
throw error;
77+
}
78+
}
79+
80+
/**
81+
* Make authenticated REST API request
82+
*/
83+
private async makeAuthenticatedRequest(url: string): Promise<any> {
84+
const token = await this.getStoredToken();
85+
86+
if (!token) {
87+
throw new Error('No authentication token available');
88+
}
89+
90+
const response = await fetch(url, {
91+
headers: {
92+
Authorization: `token ${token}`,
93+
Accept: 'application/vnd.github.v3+json',
94+
'User-Agent': 'GitHubSecurityAlerts',
95+
},
96+
});
97+
98+
if (!response.ok) {
99+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
100+
}
101+
102+
return response.json();
103+
}
104+
105+
/**
106+
* Make GraphQL request for rate limit info
107+
*/
108+
private async makeGraphQLRequest(): Promise<any> {
109+
const token = await this.getStoredToken();
110+
111+
if (!token) {
112+
throw new Error('No authentication token available');
113+
}
114+
115+
const query = `
116+
query {
117+
viewer {
118+
login
119+
}
120+
rateLimit {
121+
limit
122+
cost
123+
remaining
124+
resetAt
125+
}
126+
}
127+
`;
128+
129+
const response = await fetch(`${this.API_BASE_URL}/graphql`, {
130+
method: 'POST',
131+
headers: {
132+
Authorization: `token ${token}`,
133+
'Content-Type': 'application/json',
134+
'User-Agent': 'GitHubSecurityAlerts',
135+
},
136+
body: JSON.stringify({ query }),
137+
});
138+
139+
if (!response.ok) {
140+
throw new Error(
141+
`GraphQL HTTP ${response.status}: ${response.statusText}`
142+
);
143+
}
144+
145+
const result = await response.json();
146+
147+
if (result.errors) {
148+
throw new Error(`GraphQL Error: ${result.errors[0].message}`);
149+
}
150+
151+
return result.data;
152+
}
153+
154+
/**
155+
* Get stored authentication token from Tauri backend
156+
*/
157+
private async getStoredToken(): Promise<string | null> {
158+
try {
159+
return await this.tauriService.getToken();
160+
} catch (error) {
161+
console.error('Error getting token:', error);
162+
return null;
163+
}
164+
}
165+
166+
/**
167+
* Calculate usage percentage
168+
*/
169+
calculateUsagePercentage(used: number, limit: number): number {
170+
if (limit === 0) return 0;
171+
return Math.round((used / limit) * 100);
172+
}
173+
174+
/**
175+
* Format reset time to readable string
176+
*/
177+
formatResetTime(resetAt: string | number): string {
178+
try {
179+
let resetDate: Date;
180+
181+
// Handle different formats: ISO string, Unix timestamp, or Unix timestamp in seconds
182+
if (typeof resetAt === 'number') {
183+
// Unix timestamp (GitHub API sometimes returns this)
184+
resetDate = new Date(
185+
resetAt > 1000000000000 ? resetAt : resetAt * 1000
186+
);
187+
} else if (typeof resetAt === 'string') {
188+
// Try parsing as ISO string first, then as number
189+
if (/^\d+$/.test(resetAt)) {
190+
// String containing only numbers - treat as Unix timestamp
191+
const timestamp = parseInt(resetAt, 10);
192+
resetDate = new Date(
193+
timestamp > 1000000000000 ? timestamp : timestamp * 1000
194+
);
195+
} else {
196+
// ISO string
197+
resetDate = new Date(resetAt);
198+
}
199+
} else {
200+
return 'Unknown';
201+
}
202+
203+
// Check if the date is valid
204+
if (isNaN(resetDate.getTime())) {
205+
console.warn('Invalid reset date:', resetAt);
206+
return 'Unknown';
207+
}
208+
209+
const now = new Date();
210+
const diffMs = resetDate.getTime() - now.getTime();
211+
212+
if (diffMs <= 0) {
213+
return 'Now';
214+
}
215+
216+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
217+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
218+
219+
if (diffHours > 0) {
220+
return `${diffHours}h ${diffMinutes}m`;
221+
} else {
222+
return `${diffMinutes}m`;
223+
}
224+
} catch (error) {
225+
console.error('Error formatting reset time:', error, 'Input:', resetAt);
226+
return 'Unknown';
227+
}
228+
}
229+
}

‎src/app/core/services/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './tauri/tauri.service';
2+
export * from './github-rate-limit.service';

‎src/app/core/services/tauri/tauri.service.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export class TauriService {
4747
return invoke<AuthStatus>('get_auth_status');
4848
}
4949

50+
async getToken(): Promise<string | null> {
51+
return invoke<string | null>('get_token');
52+
}
53+
5054
async logout(): Promise<void> {
5155
return invoke('logout');
5256
}

‎src/app/shared/components/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './settings-panel/settings-panel.component';
55
export * from './repos-panel/repos-panel.component';
66
export * from './alerts-list/alerts-list.component';
77
export * from './footer/footer.component';
8+
export * from './rate-limit-status/rate-limit-status.component';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<div class="rate-limit-status" *ngIf="rateLimits">
2+
<h3 class="status-title">
3+
<i class="ti ti-api"></i>
4+
Github API Status
5+
<button class="refresh-btn" (click)="refresh()" [disabled]="isLoading">
6+
<i class="ti" [ngClass]="isLoading ? 'ti-loader spinning' : 'ti-refresh'"></i>
7+
</button>
8+
</h3>
9+
10+
<div class="status-grid">
11+
<!-- REST API -->
12+
<div class="status-card" *ngIf="rateLimits.core">
13+
<div class="card-header">
14+
<span class="api-type">REST API</span>
15+
</div>
16+
<div class="usage-bar">
17+
<div
18+
class="usage-fill"
19+
[style.width.%]="getUsagePercentage(rateLimits.core.remaining, rateLimits.core.limit)"
20+
[ngClass]="'status-' + getStatusColor(getUsagePercentage(rateLimits.core.remaining, rateLimits.core.limit))">
21+
</div>
22+
</div>
23+
<div class="usage-info">
24+
<span class="remaining">{{ rateLimits.core.remaining }}/{{ rateLimits.core.limit }}</span>
25+
<span class="reset-time">Reset in {{ formatResetTime(rateLimits.core.resetAt) }}</span>
26+
</div>
27+
</div>
28+
29+
<!-- GraphQL API -->
30+
<div class="status-card" *ngIf="rateLimits.graphql">
31+
<div class="card-header">
32+
<span class="api-type">GraphQL API</span>
33+
</div>
34+
<div class="usage-bar">
35+
<div
36+
class="usage-fill"
37+
[style.width.%]="getUsagePercentage(rateLimits.graphql.remaining, rateLimits.graphql.limit)"
38+
[ngClass]="'status-' + getStatusColor(getUsagePercentage(rateLimits.graphql.remaining, rateLimits.graphql.limit))">
39+
</div>
40+
</div>
41+
<div class="usage-info">
42+
<span class="remaining">{{ rateLimits.graphql.remaining }}/{{ rateLimits.graphql.limit }}</span>
43+
<span class="reset-time">Reset in {{ formatResetTime(rateLimits.graphql.resetAt) }}</span>
44+
</div>
45+
</div>
46+
</div>
47+
</div>
48+
49+
<div class="rate-limit-loading" *ngIf="isLoading && !rateLimits">
50+
<i class="ti ti-loader spinning"></i>
51+
<span>Loading API status...</span>
52+
</div>
53+
54+
<div class="rate-limit-error" *ngIf="error">
55+
<i class="ti ti-alert-circle"></i>
56+
<span>{{ error }}</span>
57+
</div>

0 commit comments

Comments
 (0)