Skip to content

Commit 69b1e76

Browse files
committed
add cache to avoid being banned from github for hammering the API
1 parent 5c52629 commit 69b1e76

File tree

10 files changed

+814
-168
lines changed

10 files changed

+814
-168
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ insert_final_newline = true
66
trim_trailing_whitespace = true
77

88
end_of_line = lf
9-
max_line_length = 100
9+
max_line_length = 120

.prettierrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"$schema": "https://json.schemastore.org/prettierrc",
33
"semi": true,
44
"singleQuote": true,
5-
"printWidth": 100
5+
"printWidth": 120
66
}

package-lock.json

Lines changed: 588 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"mapbox-gl": "^3.11.0",
2222
"octokit": "^5.0.3",
2323
"pinia": "^3.0.1",
24+
"pinia-plugin-persistedstate": "^4.3.0",
2425
"primeicons": "^7.0.0",
2526
"primevue": "^4.3.3",
2627
"tailwindcss": "^4.1.4",

src/App.vue

Lines changed: 36 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
<Panel
33
class="bg-transparent border-none"
44
header="Lychee Pull Requests"
5-
:pt:header:class="'justify-center text-4xl font-bold mb-8'"
5+
:pt:header:class="'justify-center text-4xl font-bold'"
66
>
77
<div class="flex flex-col max-w-4xl mx-auto gap-4 text-left">
8+
<div class="group mb-4 text-muted-color text-center cursor-pointer" @click="refresh">
9+
<span class="group-hover:hidden">Last update: {{ formattedUpdated }}</span>
10+
<span class="hidden group-hover:inline text-primary-emphasis">Click to clear cache.</span>
11+
</div>
812
<template v-if="pullRequests">
913
<template v-if="pullRequests.length === 1">
1014
<PrList v-if="pullRequests" :pull-requests="pullRequests[0].data" />
@@ -33,30 +37,29 @@
3337

3438
<script setup lang="ts">
3539
import { Octokit } from 'octokit';
36-
import { ref } from 'vue';
3740
import { onMounted } from 'vue';
3841
import { Panel } from 'primevue';
39-
import {
40-
type PullRequest,
41-
type PullRequestReview,
42-
type ReviewStatus,
43-
type User,
44-
APPROVED,
45-
CHANGES_REQUESTED,
46-
CONTRIBUTOR,
47-
} from './ResponsesTypes.ts';
42+
import { type PullRequest } from './ResponsesTypes.ts';
4843
import { useSplitter } from './composables/splitter';
4944
import { computed } from 'vue';
5045
import PrList from './components/PrList.vue';
46+
import { useQueryStore } from './stores/queryStore';
47+
import { useOctokitWrapper } from './composables/octokitWrapper';
48+
import { useGetData } from './composables/getData';
49+
import { storeToRefs } from 'pinia';
50+
51+
const queryStore = useQueryStore();
52+
const { updated } = storeToRefs(queryStore);
53+
const octokit = new Octokit();
54+
55+
const { fetchPullRequests, fetchPullRequestReviews } = useOctokitWrapper(octokit, queryStore);
56+
const { pullRequestsData, getPrs, getStatuses } = useGetData(fetchPullRequests, fetchPullRequestReviews);
5157
5258
const { spliter } = useSplitter();
53-
const pullRequestsData = ref<(PullRequest & ReviewStatus)[] | undefined>(undefined);
5459
const pullRequests = computed(() => {
5560
if (!pullRequestsData.value) return undefined;
5661
return spliter(pullRequestsData.value, prToGroup, prToGroup);
5762
});
58-
const octokit = new Octokit();
59-
6063
function prToGroup(pr: PullRequest): string {
6164
if (!pr.head.ref.includes('/')) {
6265
return 'standalone';
@@ -65,91 +68,31 @@ function prToGroup(pr: PullRequest): string {
6568
return pr.head.ref.split('/')[0] || 'standalone';
6669
}
6770
68-
async function getPrs(): Promise<void> {
69-
return octokit.rest.pulls
70-
.list({
71-
owner: 'LycheeOrg',
72-
repo: 'Lychee',
73-
})
74-
.then((response) => {
75-
pullRequestsData.value = response.data as unknown as PullRequest[];
76-
})
77-
.catch((error) => {
78-
console.error('Error fetching pull requests:', error);
79-
});
80-
}
81-
82-
async function getStatuses() {
83-
if (!pullRequestsData.value || pullRequestsData.value.length === 0) {
84-
console.warn('No pull requests available to fetch statuses for.');
85-
return;
86-
}
87-
88-
pullRequestsData.value.forEach(async (pr, idx) => {
89-
await octokit.rest.pulls
90-
.listReviews({
91-
owner: 'LycheeOrg',
92-
repo: 'Lychee',
93-
pull_number: pr.number, // Use the pull request number from the fetched PRs
94-
})
95-
.then((response) => {
96-
console.log(
97-
`Pull request reviews for PR #${pr.number} fetched successfully:`,
98-
response.data,
99-
);
100-
if (!pullRequestsData.value) {
101-
console.warn(
102-
'pullRequestsData.value is undefined, cannot update review status.',
103-
);
104-
return;
105-
}
106-
107-
pullRequestsData.value[idx].review = extractStatusForPr(
108-
response.data as PullRequestReview[],
109-
).review;
110-
})
111-
.catch((error) => {
112-
console.error(`Error fetching pull request reviews for PR #${pr.number}:`, error);
113-
});
114-
});
115-
}
116-
11771
function allDrafts(prs: PullRequest[]): boolean {
11872
return prs.every((pr) => pr.draft);
11973
}
12074
121-
function extractStatusForPr(reviews: PullRequestReview[]): ReviewStatus {
122-
// Loop through the reviews.
123-
// Ignore all the reviews that are not from the contributors.
124-
const statuses = reviews.reduce(
125-
(acc, review) => {
126-
if (
127-
(review.state === APPROVED || review.state === CHANGES_REQUESTED) &&
128-
review.author_association === CONTRIBUTOR
129-
) {
130-
acc[review.user.login] = { status: review.state, user: review.user };
131-
}
132-
return acc;
133-
},
134-
{} as Record<string, { status: string; user: User }>,
75+
const formattedUpdated = computed(() => {
76+
if (!updated.value) return '';
77+
const date = new Date(updated.value);
78+
return (
79+
date.getDate() +
80+
'/' +
81+
(date.getMonth() + 1) +
82+
'/' +
83+
date.getFullYear() +
84+
' ' +
85+
date.getHours() +
86+
':' +
87+
date.getMinutes()
13588
);
89+
});
13690
137-
const result: ReviewStatus = {
138-
review: {
139-
approved: false,
140-
changes_requested: false,
141-
by: [],
142-
},
143-
};
144-
Object.entries(statuses).forEach(([_no, review]) => {
145-
if (review.status === APPROVED) {
146-
result.review!.approved = true;
147-
} else if (status === CHANGES_REQUESTED) {
148-
result.review!.changes_requested = true;
149-
}
150-
result.review!.by.push(review.user);
151-
});
152-
return result;
91+
async function refresh() {
92+
queryStore.reset();
93+
pullRequestsData.value = undefined;
94+
await getPrs();
95+
getStatuses();
15396
}
15497
15598
onMounted(async () => {

src/ResponsesTypes.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export type User = {
77
export type PullRequest = {
88
active_lock_reason: unknown;
99
assignee: unknown;
10-
assignees: string[];
1110
author_association: string;
1211
auto_merge: null;
1312
body: string;
@@ -28,7 +27,6 @@ export type PullRequest = {
2827
node_id: string;
2928
number: number;
3029
patch_url: string;
31-
requested_reviewers: [];
3230
review_comment_url: string;
3331
review_comments_url: string;
3432
state: 'open';

src/composables/getData.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
APPROVED,
3+
CHANGES_REQUESTED,
4+
CONTRIBUTOR,
5+
type PullRequest,
6+
type PullRequestReview,
7+
type ReviewStatus,
8+
type User,
9+
} from '@/ResponsesTypes';
10+
import { ref } from 'vue';
11+
12+
export function useGetData(
13+
fetchPullRequests: (owner: string, repo: string) => Promise<PullRequest[] | undefined>,
14+
fetchPullRequestReviews: (
15+
owner: string,
16+
repo: string,
17+
pull_number: number,
18+
) => Promise<PullRequestReview[] | undefined>,
19+
) {
20+
const pullRequestsData = ref<(PullRequest & ReviewStatus)[] | undefined>(undefined);
21+
22+
async function getPrs(): Promise<void> {
23+
return fetchPullRequests('LycheeOrg', 'Lychee').then((data) => {
24+
pullRequestsData.value = data;
25+
});
26+
}
27+
28+
async function getStatuses() {
29+
if (!pullRequestsData.value || pullRequestsData.value.length === 0) {
30+
console.warn('No pull requests available to fetch statuses for.');
31+
return;
32+
}
33+
34+
pullRequestsData.value.forEach(async (pr, idx) => {
35+
await fetchPullRequestReviews('LycheeOrg', 'Lychee', pr.number)
36+
.then((data) => {
37+
if (!pullRequestsData.value) {
38+
console.warn('pullRequestsData.value is undefined, cannot update review status.');
39+
return;
40+
}
41+
if (!data) {
42+
console.warn(`No review data available for ${pr.number}.`);
43+
return;
44+
}
45+
46+
pullRequestsData.value[idx].review = extractStatusForPr(data).review;
47+
})
48+
.catch((error) => console.error(`Error fetching pull request reviews for PR #${pr.number}:`, error));
49+
});
50+
}
51+
52+
function extractStatusForPr(reviews: PullRequestReview[]): ReviewStatus {
53+
// Loop through the reviews.
54+
// Ignore all the reviews that are not from the contributors.
55+
const statuses = reviews.reduce(
56+
(acc, review) => {
57+
if (
58+
(review.state === APPROVED || review.state === CHANGES_REQUESTED) &&
59+
review.author_association === CONTRIBUTOR
60+
) {
61+
acc[review.user.login] = { status: review.state, user: review.user };
62+
}
63+
return acc;
64+
},
65+
{} as Record<string, { status: string; user: User }>,
66+
);
67+
68+
const result: ReviewStatus = {
69+
review: {
70+
approved: false,
71+
changes_requested: false,
72+
by: [],
73+
},
74+
};
75+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
76+
Object.entries(statuses).forEach(([_, review]) => {
77+
if (review.status === APPROVED) {
78+
result.review!.approved = true;
79+
} else if (status === CHANGES_REQUESTED) {
80+
result.review!.changes_requested = true;
81+
}
82+
result.review!.by.push(review.user);
83+
});
84+
return result;
85+
}
86+
87+
return {
88+
pullRequestsData,
89+
getPrs,
90+
getStatuses,
91+
};
92+
}

src/composables/octokitWrapper.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { PullRequest, PullRequestReview } from '@/ResponsesTypes';
2+
import type { QueryStore } from '@/stores/queryStore';
3+
import { type Octokit } from 'octokit';
4+
5+
export function useOctokitWrapper(octokit: Octokit, store: QueryStore) {
6+
async function fetchPullRequests(
7+
owner: string,
8+
repo: string,
9+
): Promise<PullRequest[] | undefined> {
10+
const group = {
11+
owner: owner,
12+
repo: repo,
13+
};
14+
const groupString = JSON.stringify(group);
15+
const cachedData = store.get(groupString);
16+
if (cachedData !== undefined) {
17+
return Promise.resolve(cachedData as PullRequest[]);
18+
}
19+
20+
return octokit.rest.pulls.list(group).then((response) => {
21+
const data = response.data as PullRequest[];
22+
store.save(groupString, data);
23+
return data;
24+
});
25+
}
26+
27+
async function fetchPullRequestReviews(
28+
owner: string,
29+
repo: string,
30+
pull_number: number,
31+
): Promise<PullRequestReview[] | undefined> {
32+
const group = {
33+
owner: owner,
34+
repo: repo,
35+
pull_number: pull_number,
36+
};
37+
const groupString = JSON.stringify(group);
38+
const cachedData = store.get(groupString);
39+
if (cachedData !== undefined) {
40+
return Promise.resolve(cachedData as PullRequestReview[]);
41+
}
42+
43+
return octokit.rest.pulls.listReviews(group).then((response) => {
44+
const data = response.data as PullRequestReview[];
45+
store.save(groupString, data);
46+
return data;
47+
});
48+
}
49+
50+
return {
51+
fetchPullRequests,
52+
fetchPullRequestReviews,
53+
};
54+
}

src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Aura from '@primeuix/themes/aura';
77
import Tooltip from 'primevue/tooltip';
88
import ToastService from 'primevue/toastservice';
99

10+
import piniaPluginPersistedState from 'pinia-plugin-persistedstate';
11+
1012
import App from './App.vue';
1113
import { definePreset } from '@primeuix/themes';
1214
import type { Preset } from '@primeuix/themes/types';
@@ -33,7 +35,9 @@ const preset: Preset = {
3335

3436
const MySuperSarvinPreset = definePreset(Aura, preset);
3537

36-
app.use(createPinia());
38+
const pinia = createPinia();
39+
pinia.use(piniaPluginPersistedState);
40+
app.use(pinia);
3741
app.use(PrimeVue, {
3842
theme: {
3943
preset: MySuperSarvinPreset,

0 commit comments

Comments
 (0)