agent-orchestrator/packages/web/src/components/PRStatus.tsx

188 lines
6.6 KiB
TypeScript

"use client";
import { type DashboardPR, isPRRateLimited, isPRUnenriched } from "@/lib/types";
import { CIBadge } from "./CIBadge";
export function getSizeLabel(additions: number, deletions: number): string {
const size = additions + deletions;
return size > 1000 ? "XL" : size > 500 ? "L" : size > 200 ? "M" : size > 50 ? "S" : "XS";
}
interface PRStatusProps {
pr: DashboardPR;
}
export function PRStatus({ pr }: PRStatusProps) {
const sizeLabel = getSizeLabel(pr.additions, pr.deletions);
const rateLimited = isPRRateLimited(pr);
const unenriched = isPRUnenriched(pr);
return (
<div className="flex flex-wrap items-center gap-1.5">
{/* PR number */}
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="text-[11px] font-medium text-[var(--color-accent)] underline-offset-2 hover:underline"
onClick={(e) => e.stopPropagation()}
>
#{pr.number}
</a>
{/* Size — shimmer when unenriched, hide when rate limited */}
{!rateLimited && (unenriched ? (
<span className="inline-block h-[14px] w-16 animate-pulse rounded-full bg-[var(--color-bg-subtle)]" />
) : (
<span className="inline-flex items-center rounded-full bg-[rgba(125,133,144,0.08)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-text-muted)]">
+{pr.additions} -{pr.deletions} {sizeLabel}
</span>
))}
{/* Merged badge */}
{pr.state === "merged" && (
<span className="inline-flex items-center rounded-full bg-[var(--color-chip-bg)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-text-secondary)]">
merged
</span>
)}
{/* Draft badge */}
{pr.isDraft && pr.state === "open" && (
<span className="inline-flex items-center rounded-full bg-[rgba(125,133,144,0.08)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-text-muted)]">
draft
</span>
)}
{/* CI status — shimmer when unenriched */}
{pr.state === "open" && !pr.isDraft && !rateLimited && (unenriched ? (
<span className="inline-block h-[14px] w-14 animate-pulse rounded-full bg-[var(--color-bg-subtle)]" />
) : (
<CIBadge status={pr.ciStatus} checks={pr.ciChecks} />
))}
{/* Review decision (only for open PRs with real data) */}
{pr.state === "open" && pr.reviewDecision === "approved" && !rateLimited && !unenriched && (
<span className="inline-flex items-center rounded-full bg-[rgba(63,185,80,0.1)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-accent-green)]">
approved
</span>
)}
</div>
);
}
interface PRTableRowProps {
pr: DashboardPR;
}
export function PRTableRow({ pr }: PRTableRowProps) {
const sizeLabel = getSizeLabel(pr.additions, pr.deletions);
const rateLimited = isPRRateLimited(pr);
const unenriched = isPRUnenriched(pr);
const hideData = rateLimited || unenriched;
const reviewLabel = hideData
? "—"
: pr.isDraft
? "draft"
: pr.reviewDecision === "approved"
? "approved"
: pr.reviewDecision === "changes_requested"
? "changes requested"
: "needs review";
const reviewClass = hideData
? "text-[var(--color-text-tertiary)]"
: pr.isDraft
? "text-[var(--color-text-muted)]"
: pr.reviewDecision === "approved"
? "text-[var(--color-accent-green)]"
: pr.reviewDecision === "changes_requested"
? "text-[var(--color-accent-red)]"
: "text-[var(--color-accent-yellow)]";
const shimmer = <span className="inline-block h-3 w-12 animate-pulse rounded bg-[var(--color-bg-subtle)]" />;
return (
<tr className="border-b border-[var(--color-border-muted)] hover:bg-[rgba(88,166,255,0.03)]">
<td className="px-3 py-2.5 text-sm">
<a href={pr.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
#{pr.number}
</a>
</td>
<td className="max-w-[420px] truncate px-3 py-2.5 text-sm font-medium">{pr.title}</td>
<td className="px-3 py-2.5 text-sm">
{unenriched ? shimmer : rateLimited ? (
<span className="text-[var(--color-text-tertiary)]"></span>
) : (
<>
<span className="text-[var(--color-accent-green)]">+{pr.additions}</span>{" "}
<span className="text-[var(--color-accent-red)]">-{pr.deletions}</span>{" "}
<span className="text-[var(--color-text-muted)]">{sizeLabel}</span>
</>
)}
</td>
<td className="px-3 py-2.5">
{unenriched ? shimmer : rateLimited ? (
<span className="text-[var(--color-text-tertiary)]"></span>
) : (
<CIBadge status={pr.ciStatus} checks={pr.ciChecks} compact />
)}
</td>
<td className={`px-3 py-2.5 text-xs font-semibold ${reviewClass}`}>
{unenriched ? shimmer : reviewLabel}
</td>
<td
className={`px-3 py-2.5 text-center text-sm font-bold ${unenriched ? "" : pr.unresolvedThreads > 0 ? "text-[var(--color-accent-red)]" : "text-[var(--color-border-default)]"}`}
>
{unenriched ? shimmer : pr.unresolvedThreads}
</td>
</tr>
);
}
export function PRCard({ pr }: PRTableRowProps) {
const sizeLabel = getSizeLabel(pr.additions, pr.deletions);
const rateLimited = isPRRateLimited(pr);
const unenriched = isPRUnenriched(pr);
const reviewLabel = rateLimited || unenriched
? "—"
: pr.isDraft
? "draft"
: pr.reviewDecision === "approved"
? "approved"
: pr.reviewDecision === "changes_requested"
? "changes"
: "review";
const ciLabel = rateLimited || unenriched
? "—"
: pr.ciStatus === "passing"
? "CI passing"
: pr.ciStatus === "failing"
? "CI failing"
: "CI pending";
const shimmer = <span className="inline-block h-3 w-10 animate-pulse rounded bg-[var(--color-bg-subtle)]" />;
return (
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="mobile-pr-card"
>
<div className="mobile-pr-card__line">
<span className="mobile-pr-card__number">#{pr.number}</span>
<span className="mobile-pr-card__title">{pr.title}</span>
{!rateLimited && !unenriched ? <span className="mobile-pr-card__size">{sizeLabel}</span> : null}
</div>
<div className="mobile-pr-card__meta">
<span>{unenriched ? shimmer : ciLabel}</span>
<span>{unenriched ? shimmer : reviewLabel}</span>
<span>{unenriched ? shimmer : `${pr.unresolvedThreads} threads`}</span>
</div>
</a>
);
}