Skip to content

Commit d60aabe

Browse files
committed
Merge branch 'feat/status-bar' into moc
2 parents 6f1cebf + 60f9573 commit d60aabe

File tree

15 files changed

+245
-14
lines changed

15 files changed

+245
-14
lines changed

packages/decap-cms-backend-azure/src/API.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@ export default class API {
592592
});
593593

594594
const filtered = pullRequests.filter(pr => {
595-
return pr.labels.some(label => isCMSLabel(label.name, this.cmsLabelPrefix));
595+
const labels = pr.labels ?? [];
596+
return labels.some(label => isCMSLabel(label.name, this.cmsLabelPrefix));
596597
});
597598
return filtered;
598599
}

packages/decap-cms-backend-github/src/API.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ export interface Config {
6565
cmsLabelPrefix: string;
6666
baseUrl?: string;
6767
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
68+
onRateLimitInfo?: (info: {
69+
used: number;
70+
limit: number;
71+
remaining: number;
72+
reset: number;
73+
resource: string;
74+
}) => void;
6875
}
6976

7077
interface TreeFile {
@@ -197,6 +204,13 @@ export default class API {
197204
cmsLabelPrefix: string;
198205
baseUrl?: string;
199206
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
207+
onRateLimitInfo?: (info: {
208+
used: number;
209+
limit: number;
210+
remaining: number;
211+
reset: number;
212+
resource: string;
213+
}) => void;
200214
_userPromise?: Promise<GitHubUser>;
201215
_metadataSemaphore?: Semaphore;
202216

@@ -226,6 +240,7 @@ export default class API {
226240
this.initialWorkflowStatus = config.initialWorkflowStatus;
227241
this.baseUrl = config.baseUrl;
228242
this.getUser = config.getUser;
243+
this.onRateLimitInfo = config.onRateLimitInfo;
229244
}
230245

231246
static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Decap CMS';
@@ -291,6 +306,8 @@ export default class API {
291306
}
292307

293308
parseResponse(response: Response) {
309+
this.extractRateLimitInfo(response.headers);
310+
294311
const contentType = response.headers.get('Content-Type');
295312
if (contentType && contentType.match(/json/)) {
296313
return this.parseJsonResponse(response);
@@ -304,6 +321,28 @@ export default class API {
304321
return textPromise;
305322
}
306323

324+
protected extractRateLimitInfo(headers: Headers) {
325+
if (!this.onRateLimitInfo) {
326+
return;
327+
}
328+
329+
const used = headers.get('x-ratelimit-used');
330+
const limit = headers.get('x-ratelimit-limit');
331+
const remaining = headers.get('x-ratelimit-remaining');
332+
const reset = headers.get('x-ratelimit-reset');
333+
const resource = headers.get('x-ratelimit-resource');
334+
335+
if (used && limit && remaining && reset && resource) {
336+
this.onRateLimitInfo({
337+
used: parseInt(used, 10),
338+
limit: parseInt(limit, 10),
339+
remaining: parseInt(remaining, 10),
340+
reset: parseInt(reset, 10),
341+
resource,
342+
});
343+
}
344+
}
345+
307346
handleRequestError(error: FetchError, responseStatus: number) {
308347
throw new APIError(error.message, responseStatus, API_NAME);
309348
}

packages/decap-cms-backend-github/src/GraphQLAPI.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ export default class GraphQLAPI extends API {
113113
},
114114
};
115115
});
116+
117+
// Custom fetch that captures rate limit headers
118+
// eslint-disable-next-line func-style
119+
const fetchWithRateLimit = (uri: string, options: RequestInit) => {
120+
return fetch(uri, options).then(response => {
121+
// Extract rate limit info from response headers
122+
this.extractRateLimitInfo(response.headers);
123+
return response;
124+
});
125+
};
126+
116127
// Always use direct GitHub API access for GraphQL
117128
// base_url is for OAuth endpoints only, not API requests
118129
// Ensure apiRoot is always the GitHub API endpoint
@@ -124,7 +135,7 @@ export default class GraphQLAPI extends API {
124135

125136
const httpLink = createHttpLink({
126137
uri: graphqlEndpoint,
127-
fetch, // Use global fetch
138+
fetch: fetchWithRateLimit, // Use custom fetch to capture rate limit headers
128139
});
129140

130141
// Implement intelligent cache with custom dataIdFromObject for better cache keys

packages/decap-cms-backend-github/src/implementation.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default class GitHub implements Implementation {
6969
API: API | null;
7070
useWorkflow?: boolean;
7171
initialWorkflowStatus: string;
72+
dispatch?: any;
7273
};
7374
originRepo: string;
7475
isBranchConfigured: boolean;
@@ -387,6 +388,22 @@ export default class GitHub implements Implementation {
387388
}
388389
}
389390
}
391+
392+
const onRateLimitInfo = this.options.dispatch
393+
? (info: {
394+
used: number;
395+
limit: number;
396+
remaining: number;
397+
reset: number;
398+
resource: string;
399+
}) => {
400+
this.options.dispatch!({
401+
type: 'SET_RATE_LIMIT_INFO',
402+
payload: { rateLimitInfo: info },
403+
});
404+
}
405+
: undefined;
406+
390407
const apiCtor = this.useGraphql ? GraphQLAPI : API;
391408
this.api = new apiCtor({
392409
token: this.token,
@@ -401,6 +418,7 @@ export default class GitHub implements Implementation {
401418
initialWorkflowStatus: this.options.initialWorkflowStatus,
402419
baseUrl: this.baseUrl,
403420
getUser: this.currentUser,
421+
onRateLimitInfo,
404422
});
405423
const user = await this.api!.user();
406424
const isCollab = await this.api!.hasWriteAccess().catch(error => {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Rate limit info type and callback
3+
*/
4+
export interface RateLimitCallback {
5+
(rateLimitInfo: RateLimitInfo): void;
6+
}
7+
8+
export interface RateLimitInfo {
9+
used: number;
10+
limit: number;
11+
remaining: number;
12+
reset: number;
13+
resource: string;
14+
}
15+
16+
/**
17+
* Extracts rate limit information from response headers
18+
*/
19+
export function extractRateLimitInfo(headers: Headers): RateLimitInfo | null {
20+
const used = headers.get('x-ratelimit-used');
21+
const limit = headers.get('x-ratelimit-limit');
22+
const remaining = headers.get('x-ratelimit-remaining');
23+
const reset = headers.get('x-ratelimit-reset');
24+
const resource = headers.get('x-ratelimit-resource');
25+
26+
if (!used || !limit || !remaining || !reset || !resource) {
27+
return null;
28+
}
29+
30+
return {
31+
used: parseInt(used, 10),
32+
limit: parseInt(limit, 10),
33+
remaining: parseInt(remaining, 10),
34+
reset: parseInt(reset, 10),
35+
resource,
36+
};
37+
}

packages/decap-cms-core/src/actions/auth.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,10 @@ export function logout() {
5252
} as const;
5353
}
5454

55-
// Check if user data token is cached and is valid
5655
export function authenticateUser() {
5756
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
5857
const state = getState();
59-
const backend = currentBackend(state.config);
58+
const backend = currentBackend(state.config, dispatch);
6059
dispatch(authenticating());
6160
return Promise.resolve(backend.currentUser())
6261
.then(user => {
@@ -79,7 +78,7 @@ export function authenticateUser() {
7978
export function loginUser(credentials: Credentials) {
8079
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
8180
const state = getState();
82-
const backend = currentBackend(state.config);
81+
const backend = currentBackend(state.config, dispatch);
8382

8483
dispatch(authenticating());
8584
return backend

packages/decap-cms-core/src/actions/status.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { addNotification, dismissNotification } from './notifications';
44
import type { ThunkDispatch } from 'redux-thunk';
55
import type { AnyAction } from 'redux';
66
import type { State } from '../types/redux';
7+
import type { RateLimitInfo } from '../reducers/status';
78

89
export const STATUS_REQUEST = 'STATUS_REQUEST';
910
export const STATUS_SUCCESS = 'STATUS_SUCCESS';
1011
export const STATUS_FAILURE = 'STATUS_FAILURE';
12+
export const SET_RATE_LIMIT_INFO = 'SET_RATE_LIMIT_INFO';
1113

1214
export function statusRequest() {
1315
return {
@@ -32,6 +34,13 @@ export function statusFailure(error: Error) {
3234
} as const;
3335
}
3436

37+
export function setRateLimitInfo(rateLimitInfo: RateLimitInfo) {
38+
return {
39+
type: SET_RATE_LIMIT_INFO,
40+
payload: { rateLimitInfo },
41+
} as const;
42+
}
43+
3544
export function checkBackendStatus() {
3645
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
3746
try {
@@ -95,5 +104,5 @@ export function checkBackendStatus() {
95104
}
96105

97106
export type StatusAction = ReturnType<
98-
typeof statusRequest | typeof statusSuccess | typeof statusFailure
107+
typeof statusRequest | typeof statusSuccess | typeof statusFailure | typeof setRateLimitInfo
99108
>;

packages/decap-cms-core/src/backend.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ interface BackendOptions {
259259
backendName: string;
260260
config: CmsConfig;
261261
authStore?: AuthStore;
262+
dispatch?: any;
262263
}
263264

264265
export interface MediaFile {
@@ -294,6 +295,7 @@ interface ImplementationInitOptions {
294295
useWorkflow: boolean;
295296
updateUserCredentials: (credentials: Credentials) => void;
296297
initialWorkflowStatus: string;
298+
dispatch?: any;
297299
}
298300

299301
type Implementation = BackendImplementation & {
@@ -383,14 +385,18 @@ export class Backend {
383385
user?: User | null;
384386
backupSync: AsyncLock;
385387

386-
constructor(implementation: Implementation, { backendName, authStore, config }: BackendOptions) {
388+
constructor(
389+
implementation: Implementation,
390+
{ backendName, authStore, config, dispatch }: BackendOptions,
391+
) {
387392
// We can't reliably run this on exit, so we do cleanup on load.
388393
this.deleteAnonymousBackup();
389394
this.config = config;
390395
this.implementation = implementation.init(this.config, {
391396
useWorkflow: selectUseWorkflow(this.config),
392397
updateUserCredentials: this.updateUserCredentials,
393398
initialWorkflowStatus: status.first(),
399+
dispatch,
394400
});
395401
this.backendName = backendName;
396402
this.authStore = authStore;
@@ -1498,7 +1504,7 @@ export class Backend {
14981504
}
14991505
}
15001506

1501-
export function resolveBackend(config: CmsConfig) {
1507+
export function resolveBackend(config: CmsConfig, dispatch?: any) {
15021508
if (!config.backend.name) {
15031509
throw new Error('No backend defined in configuration');
15041510
}
@@ -1510,18 +1516,18 @@ export function resolveBackend(config: CmsConfig) {
15101516
if (!backend) {
15111517
throw new Error(`Backend not found: ${name}`);
15121518
} else {
1513-
return new Backend(backend, { backendName: name, authStore, config });
1519+
return new Backend(backend, { backendName: name, authStore, config, dispatch });
15141520
}
15151521
}
15161522

15171523
export const currentBackend = (function () {
15181524
let backend: Backend;
15191525

1520-
return (config: CmsConfig) => {
1526+
return (config: CmsConfig, dispatch?: any) => {
15211527
if (backend) {
15221528
return backend;
15231529
}
15241530

1525-
return (backend = resolveBackend(config));
1531+
return (backend = resolveBackend(config, dispatch));
15261532
};
15271533
})();

packages/decap-cms-core/src/components/App/App.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Workflow from '../Workflow/Workflow';
2121
import Editor from '../Editor/Editor';
2222
import NotFoundPage from './NotFoundPage';
2323
import Header from './Header';
24+
import StatusBar from './StatusBar';
2425

2526
TopBarProgress.config({
2627
barColors: {
@@ -35,6 +36,7 @@ const AppMainContainer = styled.div`
3536
min-width: 800px;
3637
max-width: 1440px;
3738
margin: 0 auto;
39+
padding-bottom: 3rem;
3840
`;
3941

4042
const ErrorContainer = styled.div`
@@ -253,6 +255,7 @@ class App extends React.Component {
253255
</Switch>
254256
{useMediaLibrary ? <MediaLibrary /> : null}
255257
</AppMainContainer>
258+
<StatusBar />
256259
</>
257260
);
258261
}

0 commit comments

Comments
 (0)