web/app/canopy/%5Bowner%5D/%5Brepo%5D/page.tsxblame
View source
da0f6511import type { Metadata } from "next";
fe3b5092import Link from "next/link";
bc1b2ba3import { getCanopyRecentRuns } from "@/lib/grove-api";
fe3b5094import { Badge } from "@/app/components/ui/badge";
fe3b5095import { TimeAgo } from "@/app/components/time-ago";
fe3b5096import {
fe3b5097 formatTriggerType,
fe3b5098 getInvocationRunSlug,
fe3b5099 getInvocationStatus,
fe3b50910 groupByInvocation,
fe3b50911} from "@/lib/canopy-invocations";
5bcd5db12import { CanopyLiveRefresh } from "@/app/components/canopy-live-refresh";
da0f65113
da0f65114interface Props {
da0f65115 params: Promise<{ owner: string; repo: string }>;
da0f65116}
da0f65117
da0f65118export async function generateMetadata({ params }: Props): Promise<Metadata> {
86450dc19 const { repo } = await params;
86450dc20 return { title: `Canopy · ${repo}` };
da0f65121}
da0f65122
fe3b50923interface Run {
fe3b50924 id: number;
fe3b50925 pipeline_name: string;
fe3b50926 status: string;
fe3b50927 trigger_type?: string | null;
fe3b50928 commit_id: string | null;
fe3b50929 commit_message: string | null;
fe3b50930 trigger_ref?: string | null;
fe3b50931 started_at?: string | null;
fe3b50932 duration_ms?: number | null;
fe3b50933 created_at: string;
fe3b50934 repo_name: string;
fe3b50935 owner_name: string;
fe3b50936}
fe3b50937
fe3b50938const statusLabels: Record<string, string> = {
fe3b50939 pending: "Pending",
fe3b50940 running: "Running",
fe3b50941 passed: "Passed",
fe3b50942 failed: "Failed",
fe3b50943 cancelled: "Cancelled",
fe3b50944};
fe3b50945
fe3b50946async function getRepoRuns(owner: string, repo: string): Promise<Run[]> {
bc1b2ba47 const data = await getCanopyRecentRuns<Run>({ owner, limit: 50 });
bc1b2ba48 const runs = data?.runs ?? [];
bc1b2ba49 return runs.filter((r) => r.repo_name === repo);
da0f65150}
da0f65151
da0f65152export default async function CanopyRepoPage({ params }: Props) {
da0f65153 const { owner, repo } = await params;
fe3b50954 const runs = await getRepoRuns(owner, repo);
a011f1e55
fe3b50956 if (runs.length === 0) {
da0f65157 return (
da0f65158 <div className="max-w-3xl mx-auto px-4 py-6">
da0f65159 <p
da0f65160 className="text-sm py-8 text-center"
da0f65161 style={{ color: "var(--text-faint)" }}
da0f65162 >
087adca63 No builds yet. Add a{" "}
da0f65164 <code className="text-xs">.canopy/*.yml</code> file to get started.
da0f65165 </p>
da0f65166 </div>
da0f65167 );
da0f65168 }
da0f65169
fe3b50970 const invocations = groupByInvocation(runs);
fe3b50971
da0f65172 return (
da0f65173 <div className="max-w-3xl mx-auto px-4 py-6">
5bcd5db74 <CanopyLiveRefresh scope="repo" owner={owner} repo={repo} />
fe3b50975 <div>
fe3b50976 {invocations.map((invocation) => {
fe3b50977 const title =
fe3b50978 invocation.commitMessage ||
fe3b50979 (invocation.commitId
fe3b50980 ? invocation.commitId.substring(0, 8)
fe3b50981 : `${formatTriggerType(invocation.triggerType)} build`);
fe3b50982
fe3b50983 const summaryStatus = getInvocationStatus(invocation.runs);
fe3b50984 const newestRun = invocation.runs[invocation.runs.length - 1];
fe3b50985 const invocationHref = `/${owner}/${repo}/runs/${getInvocationRunSlug(invocation, invocations)}`;
fe3b50986
fe3b50987 return (
fe3b50988 <Link
fe3b50989 key={invocation.key}
fe3b50990 href={invocationHref}
fe3b50991 className="flex items-start gap-2 sm:gap-3 py-2.5 px-1 hover-row"
fe3b50992 style={{ borderBottom: "1px solid var(--divide)", textDecoration: "none", color: "inherit" }}
fe3b50993 >
fe3b50994 <span className="shrink-0 mt-0.5 sm:hidden">
fe3b50995 <Badge variant={summaryStatus} compact>
fe3b50996 {(statusLabels[summaryStatus] ?? summaryStatus).charAt(0)}
fe3b50997 </Badge>
fe3b50998 </span>
fe3b50999 <span className="shrink-0 mt-0.5 hidden sm:block">
fe3b509100 <Badge
fe3b509101 variant={summaryStatus}
fe3b509102 style={{ minWidth: "4rem", textAlign: "center" }}
fe3b509103 >
fe3b509104 {statusLabels[summaryStatus] ?? summaryStatus}
fe3b509105 </Badge>
fe3b509106 </span>
fe3b509107 <div className="min-w-0 flex-1">
fe3b509108 <div className="text-sm truncate" style={{ color: "var(--text-primary)" }}>
fe3b509109 {title}
fe3b509110 </div>
fe3b509111 <div className="flex items-center gap-2 mt-0.5 text-xs" style={{ color: "var(--text-faint)" }}>
fe3b509112 <span>{formatTriggerType(invocation.triggerType)}</span>
fe3b509113 {invocation.commitId && (
fe3b509114 <>
fe3b509115 <span>·</span>
fe3b509116 <span className="font-mono">{invocation.commitId.substring(0, 8)}</span>
fe3b509117 </>
fe3b509118 )}
fe3b509119 <span>·</span>
fe3b509120 <span>
fe3b509121 {invocation.runs.length} workflow{invocation.runs.length === 1 ? "" : "s"}
fe3b509122 </span>
fe3b509123 <span className="ml-auto shrink-0">
fe3b509124 <TimeAgo date={newestRun.created_at} />
fe3b509125 </span>
fe3b509126 </div>
fe3b509127 </div>
fe3b509128 </Link>
fe3b509129 );
fe3b509130 })}
fe3b509131 </div>
da0f651132 </div>
da0f651133 );
da0f651134}