Git Project selecto
This commit is contained in:
parent
9431816c35
commit
b92726df66
|
|
@ -22,7 +22,13 @@ from app.models.forgejo_commit_activity import ForgejoCommitDay
|
||||||
from app.models.forgejo_connections import ForgejoConnection
|
from app.models.forgejo_connections import ForgejoConnection
|
||||||
from app.models.forgejo_issues import ForgejoIssue
|
from app.models.forgejo_issues import ForgejoIssue
|
||||||
from app.models.forgejo_repositories import ForgejoRepository
|
from app.models.forgejo_repositories import ForgejoRepository
|
||||||
from app.schemas.metrics import CommitActivityDay, HeatmapDay, HeatmapResponse, LastPushRead, MetricsResponse
|
from app.schemas.metrics import (
|
||||||
|
CommitActivityDay,
|
||||||
|
HeatmapDay,
|
||||||
|
HeatmapResponse,
|
||||||
|
LastPushRead,
|
||||||
|
MetricsResponse,
|
||||||
|
)
|
||||||
from app.services.forgejo_client import ForgejoAPIClient
|
from app.services.forgejo_client import ForgejoAPIClient
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -50,7 +56,9 @@ async def _bg_fetch_line_stats(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Background task: sum commit line stats, persist per-repo day counts to DB."""
|
"""Background task: sum commit line stats, persist per-repo day counts to DB."""
|
||||||
|
|
||||||
async def _one(owner: str, repo: str, base_url: str, token: str | None) -> tuple[int, int, dict[str, int]]:
|
async def _one(
|
||||||
|
owner: str, repo: str, base_url: str, token: str | None
|
||||||
|
) -> tuple[int, int, dict[str, int]]:
|
||||||
try:
|
try:
|
||||||
async with ForgejoAPIClient(base_url=base_url, token=token) as client:
|
async with ForgejoAPIClient(base_url=base_url, token=token) as client:
|
||||||
return await client.get_commit_line_stats_since(owner, repo, since_iso)
|
return await client.get_commit_line_stats_since(owner, repo, since_iso)
|
||||||
|
|
@ -58,7 +66,9 @@ async def _bg_fetch_line_stats(
|
||||||
return 0, 0, {}
|
return 0, 0, {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = await asyncio.gather(*[_one(o, r, bu, tok) for o, r, bu, tok, _, _ in repos])
|
results = await asyncio.gather(
|
||||||
|
*[_one(o, r, bu, tok) for o, r, bu, tok, _, _ in repos]
|
||||||
|
)
|
||||||
total_adds = sum(a for a, _, _ in results)
|
total_adds = sum(a for a, _, _ in results)
|
||||||
total_dels = sum(d for _, d, _ in results)
|
total_dels = sum(d for _, d, _ in results)
|
||||||
merged_days: dict[str, int] = {}
|
merged_days: dict[str, int] = {}
|
||||||
|
|
@ -66,8 +76,11 @@ async def _bg_fetch_line_stats(
|
||||||
# Persist per-repo per-day counts to DB (upsert).
|
# Persist per-repo per-day counts to DB (upsert).
|
||||||
try:
|
try:
|
||||||
from datetime import date as _date
|
from datetime import date as _date
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
for (_, _, _, _, repo_id, org_id), (_, _, day_counts) in zip(repos, results):
|
for (_, _, _, _, repo_id, org_id), (_, _, day_counts) in zip(
|
||||||
|
repos, results
|
||||||
|
):
|
||||||
for day_str, cnt in day_counts.items():
|
for day_str, cnt in day_counts.items():
|
||||||
try:
|
try:
|
||||||
day_obj = _date.fromisoformat(day_str)
|
day_obj = _date.fromisoformat(day_str)
|
||||||
|
|
@ -96,6 +109,7 @@ async def _bg_fetch_line_stats(
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except Exception as db_exc:
|
except Exception as db_exc:
|
||||||
from app.core.logging import get_logger as _get_logger
|
from app.core.logging import get_logger as _get_logger
|
||||||
|
|
||||||
_get_logger(__name__).warning(
|
_get_logger(__name__).warning(
|
||||||
"commit_activity_db_write_failed", extra={"error": str(db_exc)}
|
"commit_activity_db_write_failed", extra={"error": str(db_exc)}
|
||||||
)
|
)
|
||||||
|
|
@ -104,10 +118,17 @@ async def _bg_fetch_line_stats(
|
||||||
for day, cnt in day_counts.items():
|
for day, cnt in day_counts.items():
|
||||||
merged_days[day] = merged_days.get(day, 0) + cnt
|
merged_days[day] = merged_days.get(day, 0) + cnt
|
||||||
|
|
||||||
_line_stats_cache[cache_key] = (_time.monotonic(), total_adds, total_dels, True, merged_days)
|
_line_stats_cache[cache_key] = (
|
||||||
|
_time.monotonic(),
|
||||||
|
total_adds,
|
||||||
|
total_dels,
|
||||||
|
True,
|
||||||
|
merged_days,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
_line_stats_fetching.discard(cache_key)
|
_line_stats_fetching.discard(cache_key)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Last-push background cache
|
# Last-push background cache
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -122,7 +143,9 @@ async def _bg_fetch_last_push(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Background task: find the most-recent commit across all tracked repos."""
|
"""Background task: find the most-recent commit across all tracked repos."""
|
||||||
|
|
||||||
async def _one(owner: str, repo: str, base_url: str, token: str | None) -> tuple[str, str, dict] | None:
|
async def _one(
|
||||||
|
owner: str, repo: str, base_url: str, token: str | None
|
||||||
|
) -> tuple[str, str, dict] | None:
|
||||||
try:
|
try:
|
||||||
async with ForgejoAPIClient(base_url=base_url, token=token) as client:
|
async with ForgejoAPIClient(base_url=base_url, token=token) as client:
|
||||||
commit = await client.get_last_commit(owner, repo)
|
commit = await client.get_last_commit(owner, repo)
|
||||||
|
|
@ -133,7 +156,9 @@ async def _bg_fetch_last_push(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = await asyncio.gather(*[_one(o, r, bu, tok) for o, r, bu, tok in repos])
|
results = await asyncio.gather(
|
||||||
|
*[_one(o, r, bu, tok) for o, r, bu, tok in repos]
|
||||||
|
)
|
||||||
best: tuple[str, str, dict] | None = None
|
best: tuple[str, str, dict] | None = None
|
||||||
best_ts: str = ""
|
best_ts: str = ""
|
||||||
for item in results:
|
for item in results:
|
||||||
|
|
@ -160,7 +185,7 @@ async def _bg_fetch_last_push(
|
||||||
or (commit_obj.get("author") or {}).get("name")
|
or (commit_obj.get("author") or {}).get("name")
|
||||||
or "unknown"
|
or "unknown"
|
||||||
)
|
)
|
||||||
author_obj = (commit_obj.get("author") or {})
|
author_obj = commit_obj.get("author") or {}
|
||||||
date_str = author_obj.get("date") or commit.get("created") or ""
|
date_str = author_obj.get("date") or commit.get("created") or ""
|
||||||
# Infer branch from commit refs if available, otherwise "—"
|
# Infer branch from commit refs if available, otherwise "—"
|
||||||
branch = (commit.get("branch") or "").strip() or "—"
|
branch = (commit.get("branch") or "").strip() or "—"
|
||||||
|
|
@ -388,9 +413,7 @@ async def get_forgejo_metrics(
|
||||||
if repo:
|
if repo:
|
||||||
repo_key = str(repo_id)
|
repo_key = str(repo_id)
|
||||||
last_sync_timestamps[repo_key] = (
|
last_sync_timestamps[repo_key] = (
|
||||||
repo.last_sync_at.isoformat()
|
repo.last_sync_at.isoformat() if repo.last_sync_at else ""
|
||||||
if repo.last_sync_at
|
|
||||||
else ""
|
|
||||||
)
|
)
|
||||||
sync_error_counts[repo_key] = 1 if repo.last_sync_error else 0
|
sync_error_counts[repo_key] = 1 if repo.last_sync_error else 0
|
||||||
|
|
||||||
|
|
@ -420,6 +443,9 @@ async def get_forgejo_metrics(
|
||||||
)
|
)
|
||||||
async def get_forgejo_heatmap(
|
async def get_forgejo_heatmap(
|
||||||
organization_id: UUID | None = Query(None, description="Filter by organisation ID"),
|
organization_id: UUID | None = Query(None, description="Filter by organisation ID"),
|
||||||
|
repository_id: UUID | None = Query(
|
||||||
|
None, description="Filter by a single repository ID"
|
||||||
|
),
|
||||||
session: AsyncSession = SESSION_DEP,
|
session: AsyncSession = SESSION_DEP,
|
||||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||||
) -> HeatmapResponse:
|
) -> HeatmapResponse:
|
||||||
|
|
@ -433,14 +459,18 @@ async def get_forgejo_heatmap(
|
||||||
# repos. 90 days is enough context for the dashboard summary numbers.
|
# repos. 90 days is enough context for the dashboard summary numbers.
|
||||||
line_stats_since = utcnow() - timedelta(days=90)
|
line_stats_since = utcnow() - timedelta(days=90)
|
||||||
|
|
||||||
# Fetch repos with their connections in one query
|
# Fetch scoped repos with their connections in one query
|
||||||
repos_with_conns = (
|
repos_statement = (
|
||||||
await session.exec(
|
|
||||||
select(ForgejoRepository, ForgejoConnection)
|
select(ForgejoRepository, ForgejoConnection)
|
||||||
.join(ForgejoConnection, ForgejoRepository.connection_id == ForgejoConnection.id)
|
.join(
|
||||||
|
ForgejoConnection, ForgejoRepository.connection_id == ForgejoConnection.id
|
||||||
|
)
|
||||||
.where(ForgejoRepository.organization_id == ctx.organization.id)
|
.where(ForgejoRepository.organization_id == ctx.organization.id)
|
||||||
)
|
)
|
||||||
).all()
|
if repository_id:
|
||||||
|
repos_statement = repos_statement.where(ForgejoRepository.id == repository_id)
|
||||||
|
|
||||||
|
repos_with_conns = (await session.exec(repos_statement)).all()
|
||||||
if not repos_with_conns:
|
if not repos_with_conns:
|
||||||
return HeatmapResponse(days=[], max_count=0)
|
return HeatmapResponse(days=[], max_count=0)
|
||||||
|
|
||||||
|
|
@ -448,7 +478,9 @@ async def get_forgejo_heatmap(
|
||||||
|
|
||||||
# Line stats — served from background cache; fire refresh if stale.
|
# Line stats — served from background cache; fire refresh if stale.
|
||||||
since_iso = line_stats_since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
since_iso = line_stats_since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
cache_key = str(ctx.organization.id)
|
cache_key = (
|
||||||
|
f"repo:{repository_id}" if repository_id else f"org:{ctx.organization.id}"
|
||||||
|
)
|
||||||
cached = _line_stats_cache.get(cache_key)
|
cached = _line_stats_cache.get(cache_key)
|
||||||
now = _time.monotonic()
|
now = _time.monotonic()
|
||||||
|
|
||||||
|
|
@ -459,13 +491,23 @@ async def get_forgejo_heatmap(
|
||||||
# Normalise base_url the same way get_forgejo_client() does, eagerly,
|
# Normalise base_url the same way get_forgejo_client() does, eagerly,
|
||||||
# so the background task never touches a potentially-closed session.
|
# so the background task never touches a potentially-closed session.
|
||||||
import re as _re
|
import re as _re
|
||||||
|
|
||||||
repo_tuples: list[tuple[str, str, str, str | None, UUID, UUID]] = []
|
repo_tuples: list[tuple[str, str, str, str | None, UUID, UUID]] = []
|
||||||
for repo, conn in repos_with_conns:
|
for repo, conn in repos_with_conns:
|
||||||
bu = (conn.base_url or "").rstrip("/")
|
bu = (conn.base_url or "").rstrip("/")
|
||||||
if "/api/v1" in bu:
|
if "/api/v1" in bu:
|
||||||
m = _re.match(r"(https?://[^/]+)", bu)
|
m = _re.match(r"(https?://[^/]+)", bu)
|
||||||
bu = m.group(1).rstrip("/") if m else bu
|
bu = m.group(1).rstrip("/") if m else bu
|
||||||
repo_tuples.append((repo.owner, repo.repo, bu, getattr(conn, "token", None), repo.id, repo.organization_id))
|
repo_tuples.append(
|
||||||
|
(
|
||||||
|
repo.owner,
|
||||||
|
repo.repo,
|
||||||
|
bu,
|
||||||
|
getattr(conn, "token", None),
|
||||||
|
repo.id,
|
||||||
|
repo.organization_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
_line_stats_fetching.add(cache_key)
|
_line_stats_fetching.add(cache_key)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
|
|
@ -485,7 +527,10 @@ async def get_forgejo_heatmap(
|
||||||
# background fetch is in progress.
|
# background fetch is in progress.
|
||||||
commit_db_rows = (
|
commit_db_rows = (
|
||||||
await session.exec(
|
await session.exec(
|
||||||
select(ForgejoCommitDay.date, func.sum(ForgejoCommitDay.commit_count).label("cnt"))
|
select(
|
||||||
|
ForgejoCommitDay.date,
|
||||||
|
func.sum(ForgejoCommitDay.commit_count).label("cnt"),
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
ForgejoCommitDay.repository_id.in_(repo_ids),
|
ForgejoCommitDay.repository_id.in_(repo_ids),
|
||||||
ForgejoCommitDay.date >= line_stats_since.date(),
|
ForgejoCommitDay.date >= line_stats_since.date(),
|
||||||
|
|
@ -496,11 +541,15 @@ async def get_forgejo_heatmap(
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if commit_db_rows:
|
if commit_db_rows:
|
||||||
days_list = [HeatmapDay(date=str(day), count=int(cnt)) for day, cnt in commit_db_rows]
|
days_list = [
|
||||||
|
HeatmapDay(date=str(day), count=int(cnt)) for day, cnt in commit_db_rows
|
||||||
|
]
|
||||||
max_count = max((d.count for d in days_list), default=0)
|
max_count = max((d.count for d in days_list), default=0)
|
||||||
elif has_line_stats and commit_day_counts:
|
elif has_line_stats and commit_day_counts:
|
||||||
# In-memory cache hit but DB write hasn't landed yet
|
# In-memory cache hit but DB write hasn't landed yet
|
||||||
days_list = [HeatmapDay(date=k, count=v) for k, v in sorted(commit_day_counts.items())]
|
days_list = [
|
||||||
|
HeatmapDay(date=k, count=v) for k, v in sorted(commit_day_counts.items())
|
||||||
|
]
|
||||||
max_count = max((d.count for d in days_list), default=0)
|
max_count = max((d.count for d in days_list), default=0)
|
||||||
else:
|
else:
|
||||||
# First-ever load — fall back to issue-event counts from DB while background fetch runs
|
# First-ever load — fall back to issue-event counts from DB while background fetch runs
|
||||||
|
|
@ -511,12 +560,14 @@ async def get_forgejo_heatmap(
|
||||||
select(
|
select(
|
||||||
sa_cast(ForgejoIssue.forgejo_created_at, SADate).label("day"),
|
sa_cast(ForgejoIssue.forgejo_created_at, SADate).label("day"),
|
||||||
func.count().label("cnt"),
|
func.count().label("cnt"),
|
||||||
).where(
|
)
|
||||||
|
.where(
|
||||||
ForgejoIssue.repository_id.in_(repo_ids),
|
ForgejoIssue.repository_id.in_(repo_ids),
|
||||||
ForgejoIssue.is_pull_request.is_(False),
|
ForgejoIssue.is_pull_request.is_(False),
|
||||||
ForgejoIssue.forgejo_created_at.is_not(None),
|
ForgejoIssue.forgejo_created_at.is_not(None),
|
||||||
ForgejoIssue.forgejo_created_at >= since,
|
ForgejoIssue.forgejo_created_at >= since,
|
||||||
).group_by(sa_cast(ForgejoIssue.forgejo_created_at, SADate))
|
)
|
||||||
|
.group_by(sa_cast(ForgejoIssue.forgejo_created_at, SADate))
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
for day, cnt in created_rows:
|
for day, cnt in created_rows:
|
||||||
|
|
@ -529,12 +580,14 @@ async def get_forgejo_heatmap(
|
||||||
select(
|
select(
|
||||||
sa_cast(ForgejoIssue.forgejo_closed_at, SADate).label("day"),
|
sa_cast(ForgejoIssue.forgejo_closed_at, SADate).label("day"),
|
||||||
func.count().label("cnt"),
|
func.count().label("cnt"),
|
||||||
).where(
|
)
|
||||||
|
.where(
|
||||||
ForgejoIssue.repository_id.in_(repo_ids),
|
ForgejoIssue.repository_id.in_(repo_ids),
|
||||||
ForgejoIssue.is_pull_request.is_(False),
|
ForgejoIssue.is_pull_request.is_(False),
|
||||||
ForgejoIssue.forgejo_closed_at.is_not(None),
|
ForgejoIssue.forgejo_closed_at.is_not(None),
|
||||||
ForgejoIssue.forgejo_closed_at >= since,
|
ForgejoIssue.forgejo_closed_at >= since,
|
||||||
).group_by(sa_cast(ForgejoIssue.forgejo_closed_at, SADate))
|
)
|
||||||
|
.group_by(sa_cast(ForgejoIssue.forgejo_closed_at, SADate))
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
for day, cnt in closed_rows:
|
for day, cnt in closed_rows:
|
||||||
|
|
@ -561,6 +614,7 @@ async def get_forgejo_heatmap(
|
||||||
)
|
)
|
||||||
async def get_last_push(
|
async def get_last_push(
|
||||||
organization_id: UUID | None = Query(None),
|
organization_id: UUID | None = Query(None),
|
||||||
|
repository_id: UUID | None = Query(None),
|
||||||
session: AsyncSession = SESSION_DEP,
|
session: AsyncSession = SESSION_DEP,
|
||||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||||
) -> LastPushRead | None:
|
) -> LastPushRead | None:
|
||||||
|
|
@ -568,18 +622,27 @@ async def get_last_push(
|
||||||
if organization_id and organization_id != ctx.organization.id:
|
if organization_id and organization_id != ctx.organization.id:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
repos_with_conns = (
|
repos_statement = (
|
||||||
await session.exec(
|
|
||||||
select(ForgejoRepository, ForgejoConnection)
|
select(ForgejoRepository, ForgejoConnection)
|
||||||
.join(ForgejoConnection, ForgejoRepository.connection_id == ForgejoConnection.id)
|
.join(
|
||||||
|
ForgejoConnection, ForgejoRepository.connection_id == ForgejoConnection.id
|
||||||
|
)
|
||||||
.where(ForgejoRepository.organization_id == ctx.organization.id)
|
.where(ForgejoRepository.organization_id == ctx.organization.id)
|
||||||
)
|
)
|
||||||
).all()
|
if repository_id:
|
||||||
|
repos_statement = repos_statement.where(ForgejoRepository.id == repository_id)
|
||||||
|
|
||||||
|
repos_with_conns = (await session.exec(repos_statement)).all()
|
||||||
if not repos_with_conns:
|
if not repos_with_conns:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
import re as _re
|
import re as _re
|
||||||
cache_key = f"last-push:{ctx.organization.id}"
|
|
||||||
|
cache_key = (
|
||||||
|
f"last-push:repo:{repository_id}"
|
||||||
|
if repository_id
|
||||||
|
else f"last-push:org:{ctx.organization.id}"
|
||||||
|
)
|
||||||
cached = _last_push_cache.get(cache_key)
|
cached = _last_push_cache.get(cache_key)
|
||||||
now = _time.monotonic()
|
now = _time.monotonic()
|
||||||
|
|
||||||
|
|
@ -592,7 +655,9 @@ async def get_last_push(
|
||||||
if "/api/v1" in bu:
|
if "/api/v1" in bu:
|
||||||
m = _re.match(r"(https?://[^/]+)", bu)
|
m = _re.match(r"(https?://[^/]+)", bu)
|
||||||
bu = m.group(1).rstrip("/") if m else bu
|
bu = m.group(1).rstrip("/") if m else bu
|
||||||
repo_tuples.append((repo.owner, repo.repo, bu, getattr(conn, "token", None)))
|
repo_tuples.append(
|
||||||
|
(repo.owner, repo.repo, bu, getattr(conn, "token", None))
|
||||||
|
)
|
||||||
|
|
||||||
_last_push_fetching.add(cache_key)
|
_last_push_fetching.add(cache_key)
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
|
|
@ -615,8 +680,12 @@ async def get_last_push(
|
||||||
)
|
)
|
||||||
async def get_commit_activity(
|
async def get_commit_activity(
|
||||||
organization_id: UUID | None = Query(None, description="Filter by organisation ID"),
|
organization_id: UUID | None = Query(None, description="Filter by organisation ID"),
|
||||||
repository_id: UUID | None = Query(None, description="Filter by a single repository ID"),
|
repository_id: UUID | None = Query(
|
||||||
days: int = Query(default=90, ge=1, le=365, description="How many days back to return"),
|
None, description="Filter by a single repository ID"
|
||||||
|
),
|
||||||
|
days: int = Query(
|
||||||
|
default=90, ge=1, le=365, description="How many days back to return"
|
||||||
|
),
|
||||||
session: AsyncSession = SESSION_DEP,
|
session: AsyncSession = SESSION_DEP,
|
||||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||||
) -> list[CommitActivityDay]:
|
) -> list[CommitActivityDay]:
|
||||||
|
|
@ -675,7 +744,9 @@ async def get_commit_activity(
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return [CommitActivityDay(date=str(day), commit_count=int(cnt)) for day, cnt in rows]
|
return [
|
||||||
|
CommitActivityDay(date=str(day), commit_count=int(cnt)) for day, cnt in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _zeroed_metrics() -> MetricsResponse:
|
def _zeroed_metrics() -> MetricsResponse:
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,6 @@ export default function DashboardPage() {
|
||||||
const { isSignedIn } = useAuth();
|
const { isSignedIn } = useAuth();
|
||||||
const [selectedForgejoRepositoryId, setSelectedForgejoRepositoryId] =
|
const [selectedForgejoRepositoryId, setSelectedForgejoRepositoryId] =
|
||||||
useState(ALL_FORGEJO_REPOSITORIES);
|
useState(ALL_FORGEJO_REPOSITORIES);
|
||||||
const [ripleyAutoSelected, setRipleyAutoSelected] = useState(false);
|
|
||||||
const [isRefreshingForgejoSync, setIsRefreshingForgejoSync] = useState(false);
|
const [isRefreshingForgejoSync, setIsRefreshingForgejoSync] = useState(false);
|
||||||
const [createForgejoIssueOpen, setCreateForgejoIssueOpen] = useState(false);
|
const [createForgejoIssueOpen, setCreateForgejoIssueOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -552,6 +551,25 @@ export default function DashboardPage() {
|
||||||
?.organization_id ?? null,
|
?.organization_id ?? null,
|
||||||
[forgejoRepositories],
|
[forgejoRepositories],
|
||||||
);
|
);
|
||||||
|
const ripleyForgejoRepository = useMemo(() => {
|
||||||
|
const project = botReportQuery.data?.project;
|
||||||
|
if (!project || forgejoRepositories.length === 0) return null;
|
||||||
|
|
||||||
|
const needle = project.toLowerCase().trim();
|
||||||
|
return (
|
||||||
|
forgejoRepositories.find((repo) => {
|
||||||
|
const repoName = repo.repo.toLowerCase().trim();
|
||||||
|
const displayName = repo.display_name.toLowerCase().trim();
|
||||||
|
return (
|
||||||
|
repoName === needle ||
|
||||||
|
displayName === needle ||
|
||||||
|
repoName.includes(needle) ||
|
||||||
|
needle.includes(repoName)
|
||||||
|
);
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}, [botReportQuery.data?.project, forgejoRepositories]);
|
||||||
|
const ripleyForgejoRepositoryId = ripleyForgejoRepository?.id ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -565,25 +583,17 @@ export default function DashboardPage() {
|
||||||
}
|
}
|
||||||
}, [forgejoRepositories, selectedForgejoRepositoryId]);
|
}, [forgejoRepositories, selectedForgejoRepositoryId]);
|
||||||
|
|
||||||
// Auto-select the Forgejo repo that matches Ripley's reported project.
|
// Keep the Git Project selector aligned to Ripley's current project.
|
||||||
// Only fires once on initial load while the user hasn't manually chosen.
|
// A user's manual mismatch stays put until Ripley reports a different project.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ripleyAutoSelected) return;
|
if (!ripleyForgejoRepositoryId) return;
|
||||||
const project = botReportQuery.data?.project;
|
setSelectedForgejoRepositoryId(ripleyForgejoRepositoryId);
|
||||||
if (!project || forgejoRepositories.length === 0) return;
|
}, [ripleyForgejoRepositoryId]);
|
||||||
if (selectedForgejoRepositoryId !== ALL_FORGEJO_REPOSITORIES) return;
|
|
||||||
const needle = project.toLowerCase().trim();
|
const isForgejoSelectionMismatched = Boolean(
|
||||||
const match = forgejoRepositories.find((repo) => {
|
ripleyForgejoRepositoryId &&
|
||||||
const repoName = repo.repo.toLowerCase().trim();
|
selectedForgejoRepositoryId !== ripleyForgejoRepositoryId,
|
||||||
const displayName = repo.display_name.toLowerCase().trim();
|
);
|
||||||
return repoName === needle || displayName === needle ||
|
|
||||||
repoName.includes(needle) || needle.includes(repoName);
|
|
||||||
});
|
|
||||||
if (match) {
|
|
||||||
setSelectedForgejoRepositoryId(match.id);
|
|
||||||
setRipleyAutoSelected(true);
|
|
||||||
}
|
|
||||||
}, [botReportQuery.data, forgejoRepositories, ripleyAutoSelected, selectedForgejoRepositoryId]);
|
|
||||||
|
|
||||||
const forgejoMetricsQuery = useQuery<ForgejoIssueMetrics | null, Error>({
|
const forgejoMetricsQuery = useQuery<ForgejoIssueMetrics | null, Error>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
|
|
@ -624,12 +634,20 @@ export default function DashboardPage() {
|
||||||
} | null,
|
} | null,
|
||||||
Error
|
Error
|
||||||
>({
|
>({
|
||||||
queryKey: ["dashboard", "forgejo", "heatmap", forgejoOrganizationId],
|
queryKey: [
|
||||||
|
"dashboard",
|
||||||
|
"forgejo",
|
||||||
|
"heatmap",
|
||||||
|
forgejoOrganizationId,
|
||||||
|
selectedForgejoRepositoryId,
|
||||||
|
],
|
||||||
enabled: Boolean(
|
enabled: Boolean(
|
||||||
isSignedIn &&
|
isSignedIn &&
|
||||||
forgejoOrganizationId &&
|
|
||||||
!forgejoRepositoriesQuery.isLoading &&
|
!forgejoRepositoriesQuery.isLoading &&
|
||||||
!forgejoRepositoriesQuery.error,
|
!forgejoRepositoriesQuery.error &&
|
||||||
|
(selectedForgejoRepositoryId === ALL_FORGEJO_REPOSITORIES
|
||||||
|
? forgejoOrganizationId
|
||||||
|
: selectedForgejoRepository),
|
||||||
),
|
),
|
||||||
refetchInterval: (query) =>
|
refetchInterval: (query) =>
|
||||||
query.state.data?.has_line_stats === false
|
query.state.data?.has_line_stats === false
|
||||||
|
|
@ -637,17 +655,38 @@ export default function DashboardPage() {
|
||||||
: FORGEJO_DASHBOARD_REFETCH_INTERVAL_MS,
|
: FORGEJO_DASHBOARD_REFETCH_INTERVAL_MS,
|
||||||
refetchOnMount: "always",
|
refetchOnMount: "always",
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
|
if (selectedForgejoRepositoryId !== ALL_FORGEJO_REPOSITORIES) {
|
||||||
|
return getForgejoHeatmap({
|
||||||
|
repository_id: selectedForgejoRepositoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!forgejoOrganizationId) return Promise.resolve(null);
|
if (!forgejoOrganizationId) return Promise.resolve(null);
|
||||||
return getForgejoHeatmap({ organization_id: forgejoOrganizationId });
|
return getForgejoHeatmap({ organization_id: forgejoOrganizationId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const forgejoLastPushQuery = useQuery({
|
const forgejoLastPushQuery = useQuery({
|
||||||
queryKey: ["dashboard", "forgejo", "last-push", forgejoOrganizationId],
|
queryKey: [
|
||||||
enabled: Boolean(isSignedIn && forgejoOrganizationId),
|
"dashboard",
|
||||||
|
"forgejo",
|
||||||
|
"last-push",
|
||||||
|
forgejoOrganizationId,
|
||||||
|
selectedForgejoRepositoryId,
|
||||||
|
],
|
||||||
|
enabled: Boolean(
|
||||||
|
isSignedIn &&
|
||||||
|
(selectedForgejoRepositoryId === ALL_FORGEJO_REPOSITORIES
|
||||||
|
? forgejoOrganizationId
|
||||||
|
: selectedForgejoRepository),
|
||||||
|
),
|
||||||
refetchInterval: FORGEJO_DASHBOARD_REFETCH_INTERVAL_MS,
|
refetchInterval: FORGEJO_DASHBOARD_REFETCH_INTERVAL_MS,
|
||||||
refetchOnMount: "always",
|
refetchOnMount: "always",
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
|
if (selectedForgejoRepositoryId !== ALL_FORGEJO_REPOSITORIES) {
|
||||||
|
return getForgejoLastPush({
|
||||||
|
repository_id: selectedForgejoRepositoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!forgejoOrganizationId) return Promise.resolve(null);
|
if (!forgejoOrganizationId) return Promise.resolve(null);
|
||||||
return getForgejoLastPush({ organization_id: forgejoOrganizationId });
|
return getForgejoLastPush({ organization_id: forgejoOrganizationId });
|
||||||
},
|
},
|
||||||
|
|
@ -1405,10 +1444,8 @@ export default function DashboardPage() {
|
||||||
repositories={forgejoRepositories}
|
repositories={forgejoRepositories}
|
||||||
metricRepositories={scopedForgejoRepositories}
|
metricRepositories={scopedForgejoRepositories}
|
||||||
selectedRepositoryId={selectedForgejoRepositoryId}
|
selectedRepositoryId={selectedForgejoRepositoryId}
|
||||||
onSelectedRepositoryChange={(id) => {
|
isSelectedRepositoryMismatched={isForgejoSelectionMismatched}
|
||||||
if (id === ALL_FORGEJO_REPOSITORIES) setRipleyAutoSelected(false);
|
onSelectedRepositoryChange={setSelectedForgejoRepositoryId}
|
||||||
setSelectedForgejoRepositoryId(id);
|
|
||||||
}}
|
|
||||||
onRefreshLastSync={handleRefreshForgejoLastSync}
|
onRefreshLastSync={handleRefreshForgejoLastSync}
|
||||||
onCreateIssue={() => setCreateForgejoIssueOpen(true)}
|
onCreateIssue={() => setCreateForgejoIssueOpen(true)}
|
||||||
isRefreshingLastSync={isRefreshingForgejoSync}
|
isRefreshingLastSync={isRefreshingForgejoSync}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ type ForgejoIssueMetricCardsProps = {
|
||||||
onSelectedRepositoryChange: (repositoryId: string) => void;
|
onSelectedRepositoryChange: (repositoryId: string) => void;
|
||||||
onRefreshLastSync: () => void;
|
onRefreshLastSync: () => void;
|
||||||
onCreateIssue: () => void;
|
onCreateIssue: () => void;
|
||||||
|
isSelectedRepositoryMismatched?: boolean;
|
||||||
isRefreshingLastSync?: boolean;
|
isRefreshingLastSync?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
|
|
@ -328,6 +329,7 @@ export function ForgejoIssueMetricCards({
|
||||||
onSelectedRepositoryChange,
|
onSelectedRepositoryChange,
|
||||||
onRefreshLastSync,
|
onRefreshLastSync,
|
||||||
onCreateIssue,
|
onCreateIssue,
|
||||||
|
isSelectedRepositoryMismatched = false,
|
||||||
isRefreshingLastSync = false,
|
isRefreshingLastSync = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error,
|
error,
|
||||||
|
|
@ -419,7 +421,13 @@ export function ForgejoIssueMetricCards({
|
||||||
onValueChange={onSelectedRepositoryChange}
|
onValueChange={onSelectedRepositoryChange}
|
||||||
disabled={repositories.length === 0}
|
disabled={repositories.length === 0}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-9 w-full min-w-[190px] rounded-lg border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 text-sm shadow-none focus:ring-offset-0 sm:w-[230px]">
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
"h-9 w-full min-w-[190px] rounded-lg border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 text-sm shadow-none focus:ring-offset-0 sm:w-[230px]",
|
||||||
|
isSelectedRepositoryMismatched &&
|
||||||
|
"border-[color:rgba(248,113,113,0.65)] bg-[color:rgba(248,113,113,0.10)] text-[color:var(--danger)] focus:ring-[color:rgba(248,113,113,0.35)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<SelectValue placeholder="All projects" />
|
<SelectValue placeholder="All projects" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent align="end">
|
<SelectContent align="end">
|
||||||
|
|
|
||||||
|
|
@ -647,10 +647,13 @@ export interface ForgejoLastPush {
|
||||||
|
|
||||||
export async function getForgejoLastPush(params?: {
|
export async function getForgejoLastPush(params?: {
|
||||||
organization_id?: string;
|
organization_id?: string;
|
||||||
|
repository_id?: string;
|
||||||
}): Promise<ForgejoLastPush | null> {
|
}): Promise<ForgejoLastPush | null> {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
if (params?.organization_id)
|
if (params?.organization_id)
|
||||||
searchParams.set("organization_id", params.organization_id);
|
searchParams.set("organization_id", params.organization_id);
|
||||||
|
if (params?.repository_id)
|
||||||
|
searchParams.set("repository_id", params.repository_id);
|
||||||
const qs = searchParams.toString();
|
const qs = searchParams.toString();
|
||||||
return fetchJson<ForgejoLastPush | null>(
|
return fetchJson<ForgejoLastPush | null>(
|
||||||
`/api/v1/forgejo/last-push${qs ? `?${qs}` : ""}`,
|
`/api/v1/forgejo/last-push${qs ? `?${qs}` : ""}`,
|
||||||
|
|
@ -667,10 +670,13 @@ export interface ForgejoHeatmapResponse {
|
||||||
|
|
||||||
export async function getForgejoHeatmap(params?: {
|
export async function getForgejoHeatmap(params?: {
|
||||||
organization_id?: string;
|
organization_id?: string;
|
||||||
|
repository_id?: string;
|
||||||
}): Promise<ForgejoHeatmapResponse> {
|
}): Promise<ForgejoHeatmapResponse> {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
if (params?.organization_id)
|
if (params?.organization_id)
|
||||||
searchParams.set("organization_id", params.organization_id);
|
searchParams.set("organization_id", params.organization_id);
|
||||||
|
if (params?.repository_id)
|
||||||
|
searchParams.set("repository_id", params.repository_id);
|
||||||
const qs = searchParams.toString();
|
const qs = searchParams.toString();
|
||||||
return fetchJson<ForgejoHeatmapResponse>(
|
return fetchJson<ForgejoHeatmapResponse>(
|
||||||
`/api/v1/forgejo/heatmap${qs ? `?${qs}` : ""}`,
|
`/api/v1/forgejo/heatmap${qs ? `?${qs}` : ""}`,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue