3.1 KB107 lines
Blame
1import type { Metadata } from "next";
2import { redirect } from "next/navigation";
3import { groveApiUrl } from "@/lib/utils";
4import {
5 formatTriggerType,
6 getInvocationRunSlug,
7 getSeedRunIdFromInvocationSlug,
8 getInvocationStatus,
9 groupByInvocation,
10} from "@/lib/canopy-invocations";
11import { InvocationDetail } from "./invocation-detail";
12import { CanopyLiveRefresh } from "@/app/components/canopy-live-refresh";
13
14interface Props {
15 params: Promise<{ owner: string; repo: string; runSlug: string }>;
16}
17
18interface Run {
19 id: number;
20 pipeline_name: string;
21 status: string;
22 trigger_type?: string | null;
23 commit_id: string | null;
24 commit_message: string | null;
25 trigger_ref?: string | null;
26 started_at?: string | null;
27 duration_ms?: number | null;
28 created_at: string;
29 repo_name: string;
30 owner_name: string;
31}
32
33export async function generateMetadata({ params }: Props): Promise<Metadata> {
34 const { repo } = await params;
35 return { title: `Run · ${repo}` };
36}
37
38async function getRuns(owner: string, repo: string): Promise<Run[]> {
39 try {
40 const res = await fetch(
41 `${groveApiUrl}/api/repos/${owner}/${repo}/canopy/runs?limit=200`,
42 { cache: "no-store" }
43 );
44 if (!res.ok) return [];
45 const data = await res.json();
46 return data.runs ?? [];
47 } catch {
48 return [];
49 }
50}
51
52export default async function CanopyInvocationPage({ params }: Props) {
53 const { owner, repo, runSlug } = await params;
54 const normalizedRunSlug = runSlug.toLowerCase();
55 const seedId = getSeedRunIdFromInvocationSlug(normalizedRunSlug);
56 const runs = await getRuns(owner, repo);
57 const invocations = groupByInvocation(runs);
58 const invocation =
59 (seedId !== null
60 ? invocations.find((group) => group.runs.some((run) => run.id === seedId))
61 : null) ??
62 invocations.find((group) => {
63 if (group.commitId) {
64 return group.commitId.toLowerCase().startsWith(normalizedRunSlug);
65 }
66 return getInvocationRunSlug(group, invocations) === normalizedRunSlug;
67 });
68
69 if (!invocation) {
70 return (
71 <div className="px-4 sm:px-6 py-6">
72 <p className="text-sm" style={{ color: "var(--text-faint)" }}>
73 Build invocation not found.
74 </p>
75 </div>
76 );
77 }
78
79 const title =
80 invocation.commitMessage ||
81 (invocation.commitId
82 ? invocation.commitId.substring(0, 8)
83 : `${formatTriggerType(invocation.triggerType)} build`);
84 const status = getInvocationStatus(invocation.runs);
85 const newestRun = invocation.runs[invocation.runs.length - 1];
86 const canonicalSlug = getInvocationRunSlug(invocation, invocations);
87 if (normalizedRunSlug !== canonicalSlug) {
88 redirect(`/${owner}/${repo}/runs/${canonicalSlug}`);
89 }
90
91 return (
92 <>
93 <CanopyLiveRefresh scope="repo" owner={owner} repo={repo} />
94 <InvocationDetail
95 owner={owner}
96 repo={repo}
97 runs={invocation.runs}
98 title={title}
99 status={status}
100 triggerType={invocation.triggerType ?? null}
101 commitId={invocation.commitId}
102 newestCreatedAt={newestRun.created_at}
103 />
104 </>
105 );
106}
107