4.6 KB144 lines
Blame
1import type { Metadata } from "next";
2import Link from "next/link";
3import { groveApiUrl } from "@/lib/utils";
4import { Badge } from "@/app/components/ui/badge";
5import { TimeAgo } from "@/app/components/time-ago";
6import {
7 formatTriggerType,
8 getInvocationRunSlug,
9 getInvocationStatus,
10 groupByInvocation,
11} from "@/lib/canopy-invocations";
12import { CanopyLiveRefresh } from "@/app/components/canopy-live-refresh";
13
14interface Props {
15 params: Promise<{ owner: string; repo: string }>;
16}
17
18export async function generateMetadata({ params }: Props): Promise<Metadata> {
19 const { repo } = await params;
20 return { title: `Canopy · ${repo}` };
21}
22
23interface Run {
24 id: number;
25 pipeline_name: string;
26 status: string;
27 trigger_type?: string | null;
28 commit_id: string | null;
29 commit_message: string | null;
30 trigger_ref?: string | null;
31 started_at?: string | null;
32 duration_ms?: number | null;
33 created_at: string;
34 repo_name: string;
35 owner_name: string;
36}
37
38const statusLabels: Record<string, string> = {
39 pending: "Pending",
40 running: "Running",
41 passed: "Passed",
42 failed: "Failed",
43 cancelled: "Cancelled",
44};
45
46async function getRepoRuns(owner: string, repo: string): Promise<Run[]> {
47 try {
48 const res = await fetch(
49 `${groveApiUrl}/api/canopy/recent-runs?owner=${encodeURIComponent(owner)}&limit=50`,
50 { cache: "no-store" },
51 );
52 if (!res.ok) return [];
53 const data = await res.json();
54 const runs: Run[] = data.runs ?? [];
55 return runs.filter((r) => r.repo_name === repo);
56 } catch {
57 return [];
58 }
59}
60
61export default async function CanopyRepoPage({ params }: Props) {
62 const { owner, repo } = await params;
63 const runs = await getRepoRuns(owner, repo);
64
65 if (runs.length === 0) {
66 return (
67 <div className="max-w-3xl mx-auto px-4 py-6">
68 <p
69 className="text-sm py-8 text-center"
70 style={{ color: "var(--text-faint)" }}
71 >
72 No builds yet. Add a{" "}
73 <code className="text-xs">.canopy/*.yml</code> file to get started.
74 </p>
75 </div>
76 );
77 }
78
79 const invocations = groupByInvocation(runs);
80
81 return (
82 <div className="max-w-3xl mx-auto px-4 py-6">
83 <CanopyLiveRefresh scope="repo" owner={owner} repo={repo} />
84 <div>
85 {invocations.map((invocation) => {
86 const title =
87 invocation.commitMessage ||
88 (invocation.commitId
89 ? invocation.commitId.substring(0, 8)
90 : `${formatTriggerType(invocation.triggerType)} build`);
91
92 const summaryStatus = getInvocationStatus(invocation.runs);
93 const newestRun = invocation.runs[invocation.runs.length - 1];
94 const invocationHref = `/${owner}/${repo}/runs/${getInvocationRunSlug(invocation, invocations)}`;
95
96 return (
97 <Link
98 key={invocation.key}
99 href={invocationHref}
100 className="flex items-start gap-2 sm:gap-3 py-2.5 px-1 hover-row"
101 style={{ borderBottom: "1px solid var(--divide)", textDecoration: "none", color: "inherit" }}
102 >
103 <span className="shrink-0 mt-0.5 sm:hidden">
104 <Badge variant={summaryStatus} compact>
105 {(statusLabels[summaryStatus] ?? summaryStatus).charAt(0)}
106 </Badge>
107 </span>
108 <span className="shrink-0 mt-0.5 hidden sm:block">
109 <Badge
110 variant={summaryStatus}
111 style={{ minWidth: "4rem", textAlign: "center" }}
112 >
113 {statusLabels[summaryStatus] ?? summaryStatus}
114 </Badge>
115 </span>
116 <div className="min-w-0 flex-1">
117 <div className="text-sm truncate" style={{ color: "var(--text-primary)" }}>
118 {title}
119 </div>
120 <div className="flex items-center gap-2 mt-0.5 text-xs" style={{ color: "var(--text-faint)" }}>
121 <span>{formatTriggerType(invocation.triggerType)}</span>
122 {invocation.commitId && (
123 <>
124 <span>·</span>
125 <span className="font-mono">{invocation.commitId.substring(0, 8)}</span>
126 </>
127 )}
128 <span>·</span>
129 <span>
130 {invocation.runs.length} workflow{invocation.runs.length === 1 ? "" : "s"}
131 </span>
132 <span className="ml-auto shrink-0">
133 <TimeAgo date={newestRun.created_at} />
134 </span>
135 </div>
136 </div>
137 </Link>
138 );
139 })}
140 </div>
141 </div>
142 );
143}
144