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
77 changes: 77 additions & 0 deletions api/leaderboard/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
import os
import re
import urllib.parse
from collections import Counter
from datetime import datetime

import requests
Expand Down Expand Up @@ -148,7 +150,82 @@ class GithubOrganisationAPI(
queryset = openlakeContributor.objects.all()
serializer_class = OL_Serializer

def _github_headers(self):
token = os.getenv("GITHUB_TOKEN", "").strip()
headers = {"Accept": "application/vnd.github+json"}
if token:
headers["Authorization"] = f"Bearer {token}"
return headers

def _get_pr_key_contributions(self, pr_key):
normalized_key = pr_key.strip().strip("[]")
if not normalized_key:
return [], None, status.HTTP_200_OK

contributions = Counter()
page = 1

while True:
query = f'org:OpenLake is:pr state:all in:title "[{normalized_key}]"'
response = requests.get(
"https://api.github.com/search/issues",
params={"q": query, "per_page": 100, "page": page},
headers=self._github_headers(),
timeout=20,
)

if response.status_code != 200:
error_detail = None
try:
error_detail = response.json()
except ValueError:
error_detail = response.text
return (
[],
{
"error": "Failed to fetch data from GitHub.",
"upstream_status": response.status_code,
"details": error_detail,
},
response.status_code,
)

payload = response.json()
items = payload.get("items", [])
for item in items:
title = item.get("title", "")
trimmed_title = title.lstrip()
if not trimmed_title.startswith(f"[{normalized_key}]"):
continue
username = item.get("user", {}).get("login")
if username:
contributions[username] += 1

if len(items) < 100:
break

page += 1

ordered_contributors = sorted(
contributions.items(), key=lambda x: x[1], reverse=True
)
return (
[
{"username": username, "contributions": contribution_count}
for username, contribution_count in ordered_contributors
],
None,
status.HTTP_200_OK,
)

def get(self, request):
pr_key = request.query_params.get("pr_key", "").strip()
if pr_key:
data, error_payload, response_status = self._get_pr_key_contributions(pr_key)
if error_payload:
return Response(error_payload, status=response_status)
return Response(data, status=response_status)

ol_contributors = openlakeContributor.objects.all()
serializer = OL_Serializer(ol_contributors, many=True)
return Response(serializer.data)
Expand Down
100 changes: 91 additions & 9 deletions app/src/components/OpenlakeTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { Switch } from "./ui/switch";
const BACKEND = import.meta.env.VITE_BACKEND;
export function OpenLakeTable({ OLUsers }) {
const [searchfield, setSearchfield] = useState("");
const [prKeyInput, setPrKeyInput] = useState("");
const [appliedPrKey, setAppliedPrKey] = useState("");
const [keyFilteredUsers, setKeyFilteredUsers] = useState([]);
const [isFetchingPrKeyData, setIsFetchingPrKeyData] = useState(false);
const [filteredusers, setFilteredusers] = useState([]);
const [todisplayusers, setTodisplayusers] = useState([]);
const [OLFriends, setOLFriends] = useState([]);
const [showOLFriends, setShowOLFriends] = useState(false);
const { open, isMobile } = useSidebar();
const sourceUsers = appliedPrKey ? keyFilteredUsers : OLUsers;
const columns = [
{
accessorKey: "username",
Expand Down Expand Up @@ -119,13 +124,52 @@ export function OpenLakeTable({ OLUsers }) {
getccfriends();
}, []);

useEffect(() => {
if (!appliedPrKey) {
setKeyFilteredUsers([]);
return;
}

const controller = new AbortController();
const { signal } = controller;

const getPrKeyFilteredUsers = async () => {
setIsFetchingPrKeyData(true);
try {
const response = await fetch(
BACKEND + `/openlake/?pr_key=${encodeURIComponent(appliedPrKey)}`,
{ signal },
);
if (!response.ok) {
setKeyFilteredUsers([]);
return;
}
const data = await response.json();
setKeyFilteredUsers(Array.isArray(data) ? data : []);
} catch (error) {
if (error.name === "AbortError") {
return;
}
setKeyFilteredUsers([]);
} finally {
setIsFetchingPrKeyData(false);
}
};

getPrKeyFilteredUsers();

return () => {
controller.abort();
};
}, [appliedPrKey]);

useEffect(() => {
if (showOLFriends) {
setTodisplayusers(
OLUsers.filter((OLUser) => OLFriends.includes(OLUser.username)),
sourceUsers.filter((OLUser) => OLFriends.includes(OLUser.username)),
);
} else {
setTodisplayusers(OLUsers);
setTodisplayusers(sourceUsers);
}
if (searchfield === "") {
setFilteredusers(todisplayusers);
Expand All @@ -138,7 +182,7 @@ export function OpenLakeTable({ OLUsers }) {
}),
);
}
}, [showOLFriends, OLFriends, searchfield, OLUsers]);
}, [showOLFriends, OLFriends, searchfield, sourceUsers]);
useEffect(() => {
if (searchfield === "") {
setFilteredusers(todisplayusers);
Expand All @@ -163,12 +207,40 @@ export function OpenLakeTable({ OLUsers }) {
}}
>
<div className="mb-2 flex flex-row justify-between">
<Input
placeholder="Search OpenLake contributors..."
className="w-[40%]"
onChange={(val) => setSearchfield(val.target.value)}
type="search"
/>
<div className="flex w-[65%] flex-row gap-2">
<Input
placeholder="Search OpenLake contributors..."
className="w-[55%]"
onChange={(val) => setSearchfield(val.target.value)}
type="search"
/>
<Input
placeholder="Filter by PR key (e.g. FOSSOVERFLOW-2025)"
className="w-[45%]"
value={prKeyInput}
onChange={(e) => setPrKeyInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setAppliedPrKey(prKeyInput.trim());
}
}}
/>
<Button
variant="outline"
onClick={() => setAppliedPrKey(prKeyInput.trim())}
>
Apply Key
</Button>
<Button
variant="ghost"
onClick={() => {
setPrKeyInput("");
setAppliedPrKey("");
}}
>
Clear
</Button>
</div>
<div>
Friends Only
<Switch
Expand All @@ -177,6 +249,16 @@ export function OpenLakeTable({ OLUsers }) {
/>
</div>
</div>
{isFetchingPrKeyData ? (
<div className="mb-2 text-sm text-muted-foreground">
Loading key-based contributions...
</div>
) : null}
{appliedPrKey ? (
<div className="mb-2 text-sm text-muted-foreground">
Showing contributions for PR title key: [{appliedPrKey}]
</div>
) : null}
<DataTable data={filteredusers} columns={columns} />
</div>
);
Expand Down
Loading