web/app/canopy/%5Bowner%5D/%5Brepo%5D/runs/%5BrunSlug%5D/page.tsxblame
View source
9d879c01import type { Metadata } from "next";
9d879c02import { redirect } from "next/navigation";
9d879c03import { groveApiUrl } from "@/lib/utils";
9d879c04import {
9d879c05 formatTriggerType,
9d879c06 getInvocationRunSlug,
9d879c07 getSeedRunIdFromInvocationSlug,
9d879c08 getInvocationStatus,
9d879c09 groupByInvocation,
9d879c010} from "@/lib/canopy-invocations";
fe3b50911import { InvocationDetail } from "./invocation-detail";
5bcd5db12import { CanopyLiveRefresh } from "@/app/components/canopy-live-refresh";
9d879c013
9d879c014interface Props {
9d879c015 params: Promise<{ owner: string; repo: string; runSlug: string }>;
9d879c016}
9d879c017
9d879c018interface Run {
9d879c019 id: number;
9d879c020 pipeline_name: string;
9d879c021 status: string;
9d879c022 trigger_type?: string | null;
9d879c023 commit_id: string | null;
9d879c024 commit_message: string | null;
9d879c025 trigger_ref?: string | null;
9d879c026 started_at?: string | null;
9d879c027 duration_ms?: number | null;
9d879c028 created_at: string;
9d879c029 repo_name: string;
9d879c030 owner_name: string;
9d879c031}
9d879c032
9d879c033export async function generateMetadata({ params }: Props): Promise<Metadata> {
86450dc34 const { repo } = await params;
86450dc35 return { title: `Run · ${repo}` };
9d879c036}
9d879c037
9d879c038async function getRuns(owner: string, repo: string): Promise<Run[]> {
9d879c039 try {
9d879c040 const res = await fetch(
9d879c041 `${groveApiUrl}/api/repos/${owner}/${repo}/canopy/runs?limit=200`,
9d879c042 { cache: "no-store" }
9d879c043 );
9d879c044 if (!res.ok) return [];
9d879c045 const data = await res.json();
9d879c046 return data.runs ?? [];
9d879c047 } catch {
9d879c048 return [];
9d879c049 }
9d879c050}
9d879c051
9d879c052export default async function CanopyInvocationPage({ params }: Props) {
9d879c053 const { owner, repo, runSlug } = await params;
9d879c054 const normalizedRunSlug = runSlug.toLowerCase();
9d879c055 const seedId = getSeedRunIdFromInvocationSlug(normalizedRunSlug);
9d879c056 const runs = await getRuns(owner, repo);
9d879c057 const invocations = groupByInvocation(runs);
9d879c058 const invocation =
9d879c059 (seedId !== null
9d879c060 ? invocations.find((group) => group.runs.some((run) => run.id === seedId))
9d879c061 : null) ??
9d879c062 invocations.find((group) => {
9d879c063 if (group.commitId) {
9d879c064 return group.commitId.toLowerCase().startsWith(normalizedRunSlug);
9d879c065 }
8a2c7d466 return getInvocationRunSlug(group, invocations) === normalizedRunSlug;
9d879c067 });
9d879c068
9d879c069 if (!invocation) {
9d879c070 return (
fe3b50971 <div className="px-4 sm:px-6 py-6">
9d879c072 <p className="text-sm" style={{ color: "var(--text-faint)" }}>
9d879c073 Build invocation not found.
9d879c074 </p>
9d879c075 </div>
9d879c076 );
9d879c077 }
9d879c078
9d879c079 const title =
9d879c080 invocation.commitMessage ||
9d879c081 (invocation.commitId
9d879c082 ? invocation.commitId.substring(0, 8)
9d879c083 : `${formatTriggerType(invocation.triggerType)} build`);
9d879c084 const status = getInvocationStatus(invocation.runs);
9d879c085 const newestRun = invocation.runs[invocation.runs.length - 1];
8a2c7d486 const canonicalSlug = getInvocationRunSlug(invocation, invocations);
9d879c087 if (normalizedRunSlug !== canonicalSlug) {
9d879c088 redirect(`/${owner}/${repo}/runs/${canonicalSlug}`);
9d879c089 }
9d879c090
9d879c091 return (
5bcd5db92 <>
5bcd5db93 <CanopyLiveRefresh scope="repo" owner={owner} repo={repo} />
5bcd5db94 <InvocationDetail
5bcd5db95 owner={owner}
5bcd5db96 repo={repo}
5bcd5db97 runs={invocation.runs}
5bcd5db98 title={title}
5bcd5db99 status={status}
5bcd5db100 triggerType={invocation.triggerType ?? null}
5bcd5db101 commitId={invocation.commitId}
5bcd5db102 newestCreatedAt={newestRun.created_at}
5bcd5db103 />
5bcd5db104 </>
9d879c0105 );
9d879c0106}