Skip to content

Commit ffa140a

Browse files
✨ Add contributions elements and some stats (#36)
* 🧪 First contributions integration * 🚧 Add elements on contributions and stats tab but some fix must be done * ✨ Add changes on github stats recuperation * 🐛 Fix date bug * ✨ complete stats and add some shorts changes
1 parent 8e20aab commit ffa140a

9 files changed

Lines changed: 373 additions & 64 deletions

File tree

src/main/java/zenika/oss/stats/config/GitHubClient.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,35 @@
1313

1414
import java.util.List;
1515

16-
@RegisterRestClient(configKey="github-api")
16+
@RegisterRestClient(configKey = "github-api")
1717
@ClientHeaderParam(name = "Accept", value = "application/vnd.github+json")
1818
@ClientHeaderParam(name = "X-GitHub-Api-Version", value = "2022-11-28")
1919
@Path("/")
2020
public interface GitHubClient {
21-
21+
2222
default String prepareToken() {
2323
String token = ConfigProvider.getConfig().getValue("github.token", String.class);
2424
return "Bearer " + token;
2525
}
2626

2727
@GET
28+
@ClientHeaderParam(name = "Authorization", value = "{zenika.oss.stats.config.GitHubClient.prepareToken}")
2829
@Path("/orgs/{organizationName}")
2930
GitHubOrganization getOrgnizationByName(@PathParam("organizationName") String organizationName);
30-
31+
3132
@GET
3233
@ClientHeaderParam(name = "Authorization", value = "{zenika.oss.stats.config.GitHubClient.prepareToken}")
3334
@Path("/orgs/{organizationName}/members")
34-
List<GitHubMember> getOrganizationMembers(@PathParam("organizationName") String organizationName, @QueryParam("per_page") int page);
35+
List<GitHubMember> getOrganizationMembers(@PathParam("organizationName") String organizationName,
36+
@QueryParam("per_page") int page);
3537

3638
@GET
39+
@ClientHeaderParam(name = "Authorization", value = "{zenika.oss.stats.config.GitHubClient.prepareToken}")
3740
@Path("/user/{login}")
3841
GitHubMember getUserInformation(@PathParam("login") String login);
3942

4043
@GET
44+
@ClientHeaderParam(name = "Authorization", value = "{zenika.oss.stats.config.GitHubClient.prepareToken}")
4145
@Path("/users/{login}/repos")
4246
List<GitHubProject> getReposForAnUser(@PathParam("login") String login);
4347
}

src/main/java/zenika/oss/stats/ressources/ContributionsRessources.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ public Response getContributionsByMember(@PathParam("memberId") String memberId)
2929
@Path("/")
3030
public Response getContributionsForAMonth(@QueryParam("year") int year, @QueryParam("month") String month) throws DatabaseException {
3131
return Response.ok(firestoreServices.getContributionsForAYearAndMonthOrderByMonth(year, month)).build();
32-
}
32+
}
33+
3334
}

src/main/java/zenika/oss/stats/services/FirestoreServices.java

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public List<ZenikaMember> getAllMembers() throws DatabaseException {
5656
* @param year : year to delete
5757
* @throws DatabaseException exception
5858
*/
59+
@CacheInvalidateAll(cacheName = "contributions-cache")
5960
public void deleteStatsForAGitHubAccountForAYear(String githubMember, int year) throws DatabaseException {
6061
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
6162
Query query = zStats.whereEqualTo("githubHandle", githubMember).whereEqualTo("year", String.valueOf(year));
@@ -81,10 +82,21 @@ public void deleteStatsForAGitHubAccountForAYear(String githubMember, int year)
8182
*
8283
* @param statsContribution : stats to save
8384
*/
85+
@CacheInvalidateAll(cacheName = "contributions-cache")
8486
public void saveStatsForAGitHubAccountForAYear(StatsContribution statsContribution) throws DatabaseException {
8587
try {
86-
// .get() waits for the operation to complete, making it synchronous and reliable
87-
firestore.collection(FirestoreCollections.STATS.value).document().set(statsContribution).get();
88+
// Use deterministic ID to prevent duplicates (Upsert behavior)
89+
String documentId = String.format("%s-%s-%s",
90+
statsContribution.getYear(),
91+
statsContribution.getMonth(),
92+
statsContribution.getGithubHandle());
93+
94+
// .get() waits for the operation to complete, making it synchronous and
95+
// reliable
96+
firestore.collection(FirestoreCollections.STATS.value)
97+
.document(documentId)
98+
.set(statsContribution)
99+
.get();
88100
} catch (InterruptedException | ExecutionException e) {
89101
throw new DatabaseException(e);
90102
}
@@ -96,6 +108,7 @@ public void saveStatsForAGitHubAccountForAYear(StatsContribution statsContributi
96108
* @param year : the year that we want to remove stats
97109
* @throws DatabaseException exception
98110
*/
111+
@CacheInvalidateAll(cacheName = "contributions-cache")
99112
public void deleteStatsForAllGitHubAccountForAYear(int year) throws DatabaseException {
100113
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
101114
Query query = zStats.whereEqualTo("year", String.valueOf(year));
@@ -105,11 +118,18 @@ public void deleteStatsForAllGitHubAccountForAYear(int year) throws DatabaseExce
105118
if (stats.isEmpty()) {
106119
return;
107120
}
108-
WriteBatch batch = firestore.batch();
109-
for (QueryDocumentSnapshot document : stats) {
110-
batch.delete(document.getReference());
121+
122+
// Handle deletion in batches of 500 (Firestore limit)
123+
final int BATCH_SIZE = 500;
124+
for (int i = 0; i < stats.size(); i += BATCH_SIZE) {
125+
WriteBatch batch = firestore.batch();
126+
List<QueryDocumentSnapshot> batchFiles = stats.subList(i, Math.min(i + BATCH_SIZE, stats.size()));
127+
128+
for (QueryDocumentSnapshot document : batchFiles) {
129+
batch.delete(document.getReference());
130+
}
131+
batch.commit().get();
111132
}
112-
batch.commit().get();
113133
} catch (InterruptedException | ExecutionException e) {
114134
throw new DatabaseException(e);
115135
}
@@ -223,6 +243,21 @@ public <T> void deleteAllDocuments(FirestoreCollections collectionType) throws D
223243
}
224244
}
225245

246+
@CacheResult(cacheName = "contributions-cache")
247+
public List<StatsContribution> getStatsForYear(int year) throws DatabaseException {
248+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
249+
Query query = zStats.whereEqualTo("year", String.valueOf(year));
250+
ApiFuture<QuerySnapshot> querySnapshot = query.get();
251+
try {
252+
return querySnapshot.get().getDocuments().stream()
253+
.map(document -> document.toObject(StatsContribution.class))
254+
.collect(Collectors.toList());
255+
} catch (InterruptedException | ExecutionException exception) {
256+
throw new DatabaseException(exception);
257+
}
258+
}
259+
260+
@CacheResult(cacheName = "contributions-cache")
226261
public List<StatsContribution> getContributionsForAMemberOrderByYear(String memberId) throws DatabaseException {
227262
List<StatsContribution> stats = null;
228263
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
@@ -259,4 +294,17 @@ public List<StatsContribution> getContributionsForAYearAndMonthOrderByMonth(int
259294

260295
return stats;
261296
}
297+
298+
@CacheResult(cacheName = "contributions-cache")
299+
public List<StatsContribution> getAllStats() throws DatabaseException {
300+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
301+
ApiFuture<QuerySnapshot> querySnapshot = zStats.get();
302+
try {
303+
return querySnapshot.get().getDocuments().stream()
304+
.map(document -> document.toObject(StatsContribution.class))
305+
.collect(Collectors.toList());
306+
} catch (InterruptedException | ExecutionException exception) {
307+
throw new DatabaseException(exception);
308+
}
309+
}
262310
}

src/main/java/zenika/oss/stats/services/GitHubServices.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package zenika.oss.stats.services;
22

3-
import io.smallrye.graphql.client.GraphQLClient;
43
import io.smallrye.graphql.client.Response;
54
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
5+
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder;
6+
import jakarta.annotation.PostConstruct;
67
import jakarta.enterprise.context.ApplicationScoped;
78
import jakarta.inject.Inject;
89
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -46,10 +47,22 @@ public class GitHubServices {
4647
@Inject
4748
GitHubGraphQLClient gitHubGraphQLClient;
4849

49-
@Inject
50-
@GraphQLClient("github-api-dynamic")
50+
@ConfigProperty(name = "quarkus.smallrye-graphql-client.github-api-dynamic.url")
51+
String graphQLUrl;
52+
53+
@ConfigProperty(name = "github.token")
54+
String githubToken;
55+
5156
DynamicGraphQLClient dynamicGraphQLClient;
5257

58+
@PostConstruct
59+
public void init() {
60+
dynamicGraphQLClient = DynamicGraphQLClientBuilder.newBuilder()
61+
.url(graphQLUrl)
62+
.header("Authorization", "Bearer " + githubToken)
63+
.build();
64+
}
65+
5366
/**
5467
* Get information for the current organization.
5568
*
@@ -130,7 +143,7 @@ public User getContributionsDataDynamic(final String login) {
130143
variables.put("login", login);
131144
String query = "query($login: String!) {\n" + " user(login: $login) {\n" +
132145
" contributionsCollection {\n"
133-
+ " totalIssueContributions,\n" +
146+
+ " totalIssueContributions,\n" +
134147
" totalCommitContributions,\n" +
135148
" totalPullRequestContributions,\n" +
136149
" totalPullRequestReviewContributions,\n" +
@@ -163,6 +176,9 @@ public List<CustomStatsContributionsUserByMonth> getContributionsForTheCurrentYe
163176
try {
164177

165178
for (Month month : Month.values()) {
179+
if (year == Year.now().getValue() && month.getValue() > LocalDate.now().getMonthValue()) {
180+
break;
181+
}
166182
var variables = new HashMap<String, Object>();
167183
variables.put("login", login);
168184

src/main/java/zenika/oss/stats/ui/tabs/ContributionsTab.java

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.javelit.core.Jt;
44
import io.javelit.core.JtContainer;
5+
import io.quarkus.logging.Log;
56
import jakarta.enterprise.context.ApplicationScoped;
67
import jakarta.inject.Inject;
78
import zenika.oss.stats.beans.CustomStatsContributionsUserByMonth;
@@ -11,8 +12,17 @@
1112
import zenika.oss.stats.services.FirestoreServices;
1213
import zenika.oss.stats.services.GitHubServices;
1314

15+
import org.icepear.echarts.Bar;
16+
import org.icepear.echarts.charts.bar.BarSeries;
17+
import org.icepear.echarts.components.coord.cartesian.CategoryAxis;
18+
import org.icepear.echarts.components.coord.cartesian.ValueAxis;
19+
20+
import java.time.Month;
1421
import java.time.Year;
22+
import java.util.ArrayList;
1523
import java.util.List;
24+
import java.util.Map;
25+
import java.util.stream.Collectors;
1626

1727
@ApplicationScoped
1828
public class ContributionsTab {
@@ -24,6 +34,7 @@ public class ContributionsTab {
2434
FirestoreServices firestoreServices;
2535

2636
public void render(JtContainer contributionsTab) {
37+
2738
var columns = Jt.columns(2).key("contributions_columns").use(contributionsTab);
2839

2940
Jt.subheader("User Contributions History").use(columns.col(0));
@@ -34,7 +45,7 @@ public void render(JtContainer contributionsTab) {
3445
.value(Year.now().getValue())
3546
.use(columns.col(1));
3647

37-
if (Jt.button("📈 Sync Contributions for " + yearValue).use(columns.col(1))) {
48+
if (Jt.button("📈 Sync Contributions for the year selected").use(columns.col(1))) {
3849
try {
3950
int year = yearValue;
4051
firestoreServices.deleteStatsForAllGitHubAccountForAYear(year);
@@ -57,13 +68,97 @@ public void render(JtContainer contributionsTab) {
5768
syncedCount++;
5869
}
5970
}
60-
Jt.success("Successfully synced contributions for " + syncedCount + " members in " + year + "!")
71+
Jt.success("Successfully synced contributions for " + syncedCount + " members in " + year + "!")
6172
.use(contributionsTab);
6273
} catch (Exception e) {
63-
Jt.error("Error syncing contributions: " + e.getMessage()).use(contributionsTab);
74+
Jt.error("❌ Error syncing contributions: " + e.getMessage()).use(contributionsTab);
75+
}
76+
}
77+
78+
Jt.subheader("Monthly Contributions in " + yearValue).use(contributionsTab);
79+
80+
try {
81+
List<StatsContribution> allStats = firestoreServices.getStatsForYear(yearValue);
82+
83+
Map<Month, Integer> contributionsByMonth = allStats.stream()
84+
.collect(Collectors.groupingBy(
85+
s -> Month.valueOf(s.getMonth().toUpperCase()),
86+
Collectors.summingInt(
87+
s -> s.getNumberOfContributionsOnGitHub() + s.getNumberOfContributionsOnGitLab())));
88+
89+
List<String> months = new ArrayList<>();
90+
List<Integer> counts = new ArrayList<>();
91+
92+
for (Month m : Month.values()) {
93+
months.add(m.name());
94+
counts.add(contributionsByMonth.getOrDefault(m, 0));
95+
}
96+
97+
Bar bar = new Bar()
98+
.setTooltip("item")
99+
.setLegend()
100+
.addXAxis(new CategoryAxis().setData(months.toArray(new String[0])))
101+
.addYAxis(new ValueAxis())
102+
.addSeries(new BarSeries()
103+
.setName("Contributions")
104+
.setData(counts.toArray(new Integer[0])));
105+
106+
Jt.echarts(bar).use(contributionsTab);
107+
108+
} catch (Exception e) {
109+
Jt.error("Could not load contributions chart: " + e.getMessage()).use(contributionsTab);
110+
}
111+
112+
Jt.subheader("Individual Member Stats for " + yearValue).use(contributionsTab);
113+
114+
try {
115+
List<ZenikaMember> members = firestoreServices.getAllMembers();
116+
Map<String, String> memberOptions = members.stream()
117+
.filter(m -> m.getGitHubAccount() != null && m.getGitHubAccount().getLogin() != null)
118+
.collect(Collectors.toMap(
119+
m -> m.getName() + " (" + m.getGitHubAccount().getLogin() + ")",
120+
m -> m.getGitHubAccount().getLogin(),
121+
(existing, replacement) -> existing));
122+
123+
String selectedMemberLabel = Jt.selectbox("", new ArrayList<>(memberOptions.keySet()))
124+
.use(contributionsTab);
125+
126+
if (selectedMemberLabel != null) {
127+
String selectedMemberHandle = memberOptions.get(selectedMemberLabel);
128+
129+
List<StatsContribution> memberStats = firestoreServices
130+
.getContributionsForAMemberOrderByYear(selectedMemberHandle);
131+
132+
Map<Month, Integer> memberContributionsByMonth = memberStats.stream()
133+
.filter(s -> String.valueOf(yearValue).equals(s.getYear()))
134+
.collect(Collectors.groupingBy(
135+
s -> Month.valueOf(s.getMonth().toUpperCase()),
136+
Collectors.summingInt(s -> s.getNumberOfContributionsOnGitHub()
137+
+ s.getNumberOfContributionsOnGitLab())));
138+
139+
List<String> memberMonths = new ArrayList<>();
140+
List<Integer> memberCounts = new ArrayList<>();
141+
142+
for (Month m : Month.values()) {
143+
memberMonths.add(m.name());
144+
memberCounts.add(memberContributionsByMonth.getOrDefault(m, 0));
145+
}
146+
147+
Bar memberBar = new Bar()
148+
.setTooltip("item")
149+
.setLegend()
150+
.addXAxis(new CategoryAxis().setData(memberMonths.toArray(new String[0])))
151+
.addYAxis(new ValueAxis())
152+
.addSeries(new BarSeries()
153+
.setName(selectedMemberHandle)
154+
.setData(memberCounts.toArray(new Integer[0])));
155+
156+
Jt.echarts(memberBar).use(contributionsTab);
64157
}
158+
159+
} catch (Exception e) {
160+
Jt.error("Error loading member stats: " + e.getMessage()).use(contributionsTab);
65161
}
66162

67-
Jt.text("Fetch and save contribution statistics for the specified year.").use(contributionsTab);
68163
}
69164
}

0 commit comments

Comments
 (0)