web/app/canopy/%5Bowner%5D/page.tsxblame
View source
fe3b5091import 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";
fe3b50913
fe3b50914interface Props {
fe3b50915 params: Promise<{ owner: string }>;
fe3b50916}
fe3b50917
fe3b50918export async function generateMetadata({ params }: Props): Promise<Metadata> {
fe3b50919 const { owner } = await params;
fe3b50920 return { title: `${owner} · Canopy` };
fe3b50921}
fe3b50922
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 getOwnerRuns(owner: string): Promise<Run[]> {
bc1b2ba47 const data = await getCanopyRecentRuns<Run>({ owner, limit: 50 });
bc1b2ba48 return data?.runs ?? [];
fe3b50949}
fe3b50950
fe3b50951function groupByRepo(runs: Run[]) {
fe3b50952 const groups = new Map<string, { owner: string; repo: string; runs: Run[]; newestRunId: number }>();
fe3b50953 const sorted = [...runs].sort((a, b) => b.id - a.id);
fe3b50954 for (const run of sorted) {
fe3b50955 const key = run.repo_name;
fe3b50956 let group = groups.get(key);
fe3b50957 if (!group) {
fe3b50958 group = { owner: run.owner_name, repo: run.repo_name, runs: [], newestRunId: run.id };
fe3b50959 groups.set(key, group);
fe3b50960 }
fe3b50961 group.runs.push(run);
fe3b50962 }
fe3b50963 return Array.from(groups.values()).sort((a, b) => b.newestRunId - a.newestRunId);
fe3b50964}
fe3b50965
fe3b50966export default async function CanopyOwnerPage({ params }: Props) {
fe3b50967 const { owner } = await params;
fe3b50968 const runs = await getOwnerRuns(owner);
fe3b50969
fe3b50970 if (runs.length === 0) {
fe3b50971 return (
fe3b50972 <div className="max-w-3xl mx-auto px-4 py-6">
fe3b50973 <p
fe3b50974 className="text-sm py-8 text-center"
fe3b50975 style={{ color: "var(--text-faint)" }}
fe3b50976 >
fe3b50977 No builds yet for {owner}.
fe3b50978 </p>
fe3b50979 </div>
fe3b50980 );
fe3b50981 }
fe3b50982
fe3b50983 const repos = groupByRepo(runs);
fe3b50984
fe3b50985 return (
fe3b50986 <div className="max-w-3xl mx-auto px-4 py-6 flex flex-col gap-6">
5bcd5db87 <CanopyLiveRefresh scope="global" />
fe3b50988 {repos.map((group) => (
fe3b50989 <div key={group.repo}>
fe3b50990 <div className="pb-2" style={{ borderBottom: "1px solid var(--divide)" }}>
fe3b50991 <Link
fe3b50992 href={`/${group.owner}/${group.repo}`}
fe3b50993 className="text-sm font-medium hover:underline"
fe3b50994 style={{ color: "var(--text-primary)" }}
fe3b50995 >
fe3b50996 {group.repo}
fe3b50997 </Link>
fe3b50998 </div>
fe3b50999 <div>
fe3b509100 {(() => {
fe3b509101 const invocations = groupByInvocation(group.runs);
fe3b509102 return invocations.slice(0, 5).map((invocation) => {
fe3b509103 const title =
fe3b509104 invocation.commitMessage ||
fe3b509105 (invocation.commitId
fe3b509106 ? invocation.commitId.substring(0, 8)
fe3b509107 : `${formatTriggerType(invocation.triggerType)} build`);
fe3b509108
fe3b509109 const summaryStatus = getInvocationStatus(invocation.runs);
fe3b509110 const newestRun = invocation.runs[invocation.runs.length - 1];
fe3b509111 const invocationHref = `/${group.owner}/${group.repo}/runs/${getInvocationRunSlug(invocation, invocations)}`;
fe3b509112
fe3b509113 return (
fe3b509114 <Link
fe3b509115 key={invocation.key}
fe3b509116 href={invocationHref}
fe3b509117 className="flex items-start gap-2 sm:gap-3 py-2.5 px-1 hover-row"
fe3b509118 style={{ borderBottom: "1px solid var(--divide)", textDecoration: "none", color: "inherit" }}
fe3b509119 >
fe3b509120 <span className="shrink-0 mt-0.5 sm:hidden">
fe3b509121 <Badge variant={summaryStatus} compact>
fe3b509122 {(statusLabels[summaryStatus] ?? summaryStatus).charAt(0)}
fe3b509123 </Badge>
fe3b509124 </span>
fe3b509125 <span className="shrink-0 mt-0.5 hidden sm:block">
fe3b509126 <Badge
fe3b509127 variant={summaryStatus}
fe3b509128 style={{ minWidth: "4rem", textAlign: "center" }}
fe3b509129 >
fe3b509130 {statusLabels[summaryStatus] ?? summaryStatus}
fe3b509131 </Badge>
fe3b509132 </span>
fe3b509133 <div className="min-w-0 flex-1">
fe3b509134 <div className="text-sm truncate" style={{ color: "var(--text-primary)" }}>
fe3b509135 {title}
fe3b509136 </div>
fe3b509137 <div className="flex items-center gap-2 mt-0.5 text-xs" style={{ color: "var(--text-faint)" }}>
fe3b509138 <span>{formatTriggerType(invocation.triggerType)}</span>
fe3b509139 {invocation.commitId && (
fe3b509140 <>
fe3b509141 <span>·</span>
fe3b509142 <span className="font-mono">{invocation.commitId.substring(0, 8)}</span>
fe3b509143 </>
fe3b509144 )}
fe3b509145 <span>·</span>
fe3b509146 <span>
fe3b509147 {invocation.runs.length} workflow{invocation.runs.length === 1 ? "" : "s"}
fe3b509148 </span>
fe3b509149 <span className="ml-auto shrink-0">
fe3b509150 <TimeAgo date={newestRun.created_at} />
fe3b509151 </span>
fe3b509152 </div>
fe3b509153 </div>
fe3b509154 </Link>
fe3b509155 );
fe3b509156 });
fe3b509157 })()}
fe3b509158 </div>
fe3b509159 <div className="pt-1.5">
fe3b509160 <Link
fe3b509161 href={`/${group.owner}/${group.repo}`}
fe3b509162 className="text-xs hover:underline"
fe3b509163 style={{ color: "var(--text-muted)" }}
fe3b509164 >
fe3b509165 View all builds
fe3b509166 </Link>
fe3b509167 </div>
fe3b509168 </div>
fe3b509169 ))}
fe3b509170 </div>
fe3b509171 );
fe3b509172}