web/app/canopy/%5Bowner%5D/%5Brepo%5D/runs/%5BrunSlug%5D/invocation-detail.tsxblame
View source
fe3b5091"use client";
fe3b5092
fe3b5093import { useState } from "react";
fe3b5094import Link from "next/link";
fe3b5095import { Badge } from "@/app/components/ui/badge";
fe3b5096import { ElapsedTime } from "@/app/components/elapsed-time";
fe3b5097import { TimeAgo } from "@/app/components/time-ago";
fe3b5098import { PipelineRunDetail } from "@/app/components/pipeline-run-detail";
fe3b5099import { formatTriggerType } from "@/lib/canopy-invocations";
fe3b50910
fe3b50911interface Run {
fe3b50912 id: number;
fe3b50913 pipeline_name: string;
fe3b50914 status: string;
fe3b50915 started_at?: string | null;
fe3b50916 duration_ms?: number | null;
fe3b50917 created_at: string;
fe3b50918}
fe3b50919
fe3b50920interface InvocationDetailProps {
fe3b50921 owner: string;
fe3b50922 repo: string;
fe3b50923 runs: Run[];
fe3b50924 title: string;
fe3b50925 status: string;
fe3b50926 triggerType: string | null;
fe3b50927 commitId: string | null;
fe3b50928 newestCreatedAt: string;
fe3b50929}
fe3b50930
fe3b50931const statusLabels: Record<string, string> = {
fe3b50932 pending: "Pending",
fe3b50933 running: "Running",
fe3b50934 passed: "Passed",
fe3b50935 failed: "Failed",
fe3b50936 cancelled: "Cancelled",
fe3b50937};
fe3b50938
fe3b50939function formatDuration(ms: number | null | undefined): string {
fe3b50940 if (!ms) return "";
fe3b50941 if (ms < 1000) return "<1s";
fe3b50942 const s = Math.floor(ms / 1000);
fe3b50943 if (s < 60) return `${s}s`;
fe3b50944 const m = Math.floor(s / 60);
fe3b50945 const rem = s % 60;
fe3b50946 return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
fe3b50947}
fe3b50948
fe3b50949export function InvocationDetail({
fe3b50950 owner,
fe3b50951 repo,
fe3b50952 runs,
fe3b50953 title,
fe3b50954 status,
fe3b50955 triggerType,
fe3b50956 commitId,
fe3b50957 newestCreatedAt,
fe3b50958}: InvocationDetailProps) {
fe3b50959 const defaultRun =
fe3b50960 runs.find((r) => r.status === "running") ??
fe3b50961 runs.find((r) => r.status === "pending") ??
fe3b50962 runs[0];
fe3b50963 const [selectedRunId, setSelectedRunId] = useState<number | undefined>(
fe3b50964 defaultRun?.id
fe3b50965 );
fe3b50966
fe3b50967 return (
fe3b50968 <>
fe3b50969 {/* Mobile */}
fe3b50970 <div className="md:hidden px-4 py-6">
fe3b50971 <div className="mb-4">
fe3b50972 <div className="flex items-center gap-2 min-w-0">
fe3b50973 <Badge variant={status} compact>
fe3b50974 {(statusLabels[status] ?? status).charAt(0)}
fe3b50975 </Badge>
fe3b50976 <span className="text-lg truncate">{title}</span>
fe3b50977 </div>
fe3b50978 <div
fe3b50979 className="text-xs flex items-center gap-2 mt-1.5"
fe3b50980 style={{ color: "var(--text-faint)" }}
fe3b50981 >
fe3b50982 <span>{formatTriggerType(triggerType)}</span>
fe3b50983 {commitId && <span className="font-mono">{commitId.substring(0, 8)}</span>}
fe3b50984 <span>
fe3b50985 {runs.length} workflow{runs.length === 1 ? "" : "s"}
fe3b50986 </span>
fe3b50987 </div>
fe3b50988 </div>
fe3b50989 {runs.map((run) => (
fe3b50990 <Link
fe3b50991 key={run.id}
fe3b50992 href={`/${owner}/${repo}/builds/${run.id}`}
fe3b50993 className="flex items-center gap-2 py-2.5 px-1 hover-row"
fe3b50994 style={{
fe3b50995 borderBottom: "1px solid var(--divide)",
fe3b50996 textDecoration: "none",
fe3b50997 color: "inherit",
fe3b50998 }}
fe3b50999 >
fe3b509100 <span className="shrink-0 sm:hidden">
fe3b509101 <Badge variant={run.status} compact>
fe3b509102 {(statusLabels[run.status] ?? run.status).charAt(0)}
fe3b509103 </Badge>
fe3b509104 </span>
fe3b509105 <span className="shrink-0 hidden sm:block">
fe3b509106 <Badge
fe3b509107 variant={run.status}
fe3b509108 style={{ minWidth: "4rem", textAlign: "center" }}
fe3b509109 >
fe3b509110 {statusLabels[run.status] ?? run.status}
fe3b509111 </Badge>
fe3b509112 </span>
fe3b509113 <div className="min-w-0 flex-1">
fe3b509114 <div
fe3b509115 className="text-sm truncate"
fe3b509116 style={{ color: "var(--accent)" }}
fe3b509117 >
fe3b509118 {run.pipeline_name}
fe3b509119 </div>
fe3b509120 <div
fe3b509121 className="flex items-center gap-2 mt-0.5 text-xs"
fe3b509122 style={{ color: "var(--text-faint)" }}
fe3b509123 >
fe3b509124 <span className="font-mono">#{run.id}</span>
fe3b509125 <span>
fe3b509126 {run.status === "running" && run.started_at ? (
fe3b509127 <ElapsedTime since={run.started_at} />
fe3b509128 ) : (
fe3b509129 formatDuration(run.duration_ms)
fe3b509130 )}
fe3b509131 </span>
fe3b509132 <span className="ml-auto shrink-0">
fe3b509133 <TimeAgo date={run.created_at} />
fe3b509134 </span>
fe3b509135 </div>
fe3b509136 </div>
fe3b509137 </Link>
fe3b509138 ))}
fe3b509139 </div>
fe3b509140
fe3b509141 {/* Desktop: master-detail */}
fe3b509142 <div className="hidden md:flex" style={{ height: "100%" }}>
fe3b509143 {selectedRunId != null ? (
fe3b509144 <PipelineRunDetail
fe3b509145 key={selectedRunId}
fe3b509146 owner={owner}
fe3b509147 repo={repo}
fe3b509148 runId={String(selectedRunId)}
fe3b509149 embedded
fe3b509150 renderLayout={(stepsColumn, logsColumn, actions) => (
fe3b509151 <>
fe3b509152 {/* Left panel: header + workflows + steps */}
fe3b509153 <div
fe3b509154 className="shrink-0 flex flex-col"
fe3b509155 style={{
fe3b509156 width: "40rem",
fe3b509157 borderRight: "1px solid var(--border-subtle)",
fe3b509158 }}
fe3b509159 >
fe3b509160 {/* Invocation header */}
fe3b509161 <div
fe3b509162 className="px-4 py-3"
fe3b509163 style={{
fe3b509164 borderBottom: "1px solid var(--border-subtle)",
fe3b509165 }}
fe3b509166 >
fe3b509167 <div className="flex items-start gap-2 min-w-0">
fe3b509168 <span className="text-sm font-medium">{title}</span>
fe3b509169 </div>
fe3b509170 <div
fe3b509171 className="flex items-center gap-2 text-xs mt-1"
fe3b509172 style={{ color: "var(--text-faint)" }}
fe3b509173 >
fe3b509174 <span>{formatTriggerType(triggerType)}</span>
fe3b509175 {commitId && (
fe3b509176 <>
fe3b509177 <span>·</span>
fe3b509178 <span className="font-mono">{commitId.substring(0, 8)}</span>
fe3b509179 </>
fe3b509180 )}
fe3b509181 <span>·</span>
fe3b509182 <TimeAgo date={newestCreatedAt} />
fe3b509183 </div>
fe3b509184 </div>
fe3b509185
fe3b509186 {/* Workflows + Steps side by side */}
fe3b509187 <div className="flex flex-1 min-h-0">
fe3b509188 <nav
fe3b509189 className="shrink-0 overflow-y-auto"
fe3b509190 style={{
fe3b509191 width: "20rem",
fe3b509192 borderRight: "1px solid var(--border-subtle)",
fe3b509193 }}
fe3b509194 >
fe3b509195 <div className="px-4 py-1.5 text-xs" style={{ color: "var(--text-faint)", borderBottom: "1px solid var(--divide)" }}>
fe3b509196 Workflows
fe3b509197 </div>
fe3b509198 {runs.map((run) => {
fe3b509199 const active = run.id === selectedRunId;
fe3b509200 const isRunning = run.status === "running";
fe3b509201 return (
fe3b509202 <div
fe3b509203 key={run.id}
fe3b509204 className="flex items-center gap-2 hover-row py-2 px-4 text-sm"
fe3b509205 style={{
fe3b509206 backgroundColor: active ? "var(--bg-inset)" : "transparent",
fe3b509207 cursor: "pointer",
fe3b509208 color: active ? "var(--text-primary)" : "var(--text-muted)",
fe3b509209 fontWeight: active ? 600 : 400,
fe3b509210 }}
fe3b509211 onClick={() => setSelectedRunId(run.id)}
fe3b509212 >
fe3b509213 <Badge variant={run.status} compact>
fe3b509214 {(statusLabels[run.status] ?? run.status).charAt(0)}
fe3b509215 </Badge>
fe3b509216 <span className="truncate" style={{ flex: 1 }}>
fe3b509217 {run.pipeline_name}
fe3b509218 </span>
fe3b509219 {isRunning && active && actions}
fe3b509220 <span className="font-mono" style={{ color: "var(--text-faint)", fontSize: "0.7rem", flexShrink: 0 }}>#{run.id}</span>
fe3b509221 </div>
fe3b509222 );
fe3b509223 })}
fe3b509224 </nav>
fe3b509225
fe3b509226 {stepsColumn}
fe3b509227 </div>
fe3b509228 </div>
fe3b509229
fe3b509230 {/* Logs (full height, right side) */}
fe3b509231 {logsColumn}
fe3b509232 </>
fe3b509233 )}
fe3b509234 />
fe3b509235 ) : (
fe3b509236 /* No run selected — just show left panel */
fe3b509237 <div
fe3b509238 className="shrink-0 flex flex-col"
fe3b509239 style={{
fe3b509240 width: "40rem",
fe3b509241 borderRight: "1px solid var(--border-subtle)",
fe3b509242 }}
fe3b509243 >
fe3b509244 <div
fe3b509245 className="px-4 py-3"
fe3b509246 style={{
fe3b509247 borderBottom: "1px solid var(--border-subtle)",
fe3b509248 backgroundColor: "var(--bg-card)",
fe3b509249 }}
fe3b509250 >
fe3b509251 <div className="flex items-center gap-2 min-w-0">
fe3b509252 <span className="text-sm font-medium">{title}</span>
fe3b509253 </div>
fe3b509254 <div
fe3b509255 className="flex items-center gap-2 text-xs mt-1"
fe3b509256 style={{ color: "var(--text-faint)" }}
fe3b509257 >
fe3b509258 <span>{formatTriggerType(triggerType)}</span>
fe3b509259 {commitId && (
fe3b509260 <>
fe3b509261 <span>·</span>
fe3b509262 <span className="font-mono">{commitId.substring(0, 8)}</span>
fe3b509263 </>
fe3b509264 )}
fe3b509265 <span>·</span>
fe3b509266 <TimeAgo date={newestCreatedAt} />
fe3b509267 </div>
fe3b509268 </div>
fe3b509269 <nav className="flex-1 overflow-y-auto">
fe3b509270 <div className="px-4 py-1.5 text-xs" style={{ color: "var(--text-faint)", borderBottom: "1px solid var(--divide)" }}>
fe3b509271 Workflows
fe3b509272 </div>
fe3b509273 {runs.map((run) => {
fe3b509274 const active = run.id === selectedRunId;
fe3b509275 return (
fe3b509276 <button
fe3b509277 key={run.id}
fe3b509278 onClick={() => setSelectedRunId(run.id)}
fe3b509279 className="w-full text-left py-2 px-4 hover-row"
fe3b509280 style={{
fe3b509281 backgroundColor: active ? "var(--bg-inset)" : "transparent",
fe3b509282 border: "none",
fe3b509283 cursor: "pointer",
fe3b509284 font: "inherit",
fe3b509285 color: active ? "var(--text-primary)" : "var(--text-muted)",
fe3b509286 fontWeight: active ? 600 : 400,
fe3b509287 }}
fe3b509288 >
fe3b509289 <div className="flex items-center gap-2 text-sm">
fe3b509290 <Badge variant={run.status} compact>
fe3b509291 {(statusLabels[run.status] ?? run.status).charAt(0)}
fe3b509292 </Badge>
fe3b509293 <span className="truncate" style={{ flex: 1 }}>{run.pipeline_name}</span>
fe3b509294 <span className="font-mono" style={{ color: "var(--text-faint)", fontSize: "0.7rem" }}>#{run.id}</span>
fe3b509295 </div>
fe3b509296 </button>
fe3b509297 );
fe3b509298 })}
fe3b509299 </nav>
fe3b509300 </div>
fe3b509301 )}
fe3b509302 </div>
fe3b509303 </>
fe3b509304 );
fe3b509305}