page.tsx94 lines · main
1import { apiJson } from '../../../../../../lib/api';
2
3interface Deployment {
4 id: string;
5 status: 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
6 createdAt: string;
7 startedAt: string | null;
8 finishedAt: string | null;
9 functionCount: string | null;
10 schemaDiffSummary: Record<string, number> | null;
11 errorCode: string | null;
12 errorMessage: string | null;
13}
14
15export const dynamic = 'force-dynamic';
16
17export default async function DeploymentsPage({ params }: { params: Promise<{ id: string }> }) {
18 const { id } = await params;
19 const { deployments } = await apiJson<{ deployments: Deployment[] }>(
20 `/v1/projects/${id}/deployments?limit=100`,
21 );
22
23 if (deployments.length === 0) {
24 return (
25 <p className="rounded-md border border-dashed border-[var(--color-border)] p-10 text-center font-mono text-sm text-[var(--color-text-muted)]">
26 no deployments yet. run <code className="text-[var(--color-text)]">briven deploy</code> from
27 your project directory.
28 </p>
29 );
30 }
31
32 return (
33 <ul className="flex flex-col gap-2">
34 {deployments.map((d) => (
35 <li
36 key={d.id}
37 className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] px-4 py-3"
38 >
39 <div className="flex items-center justify-between">
40 <p className="font-mono text-sm">{d.id}</p>
41 <span className={`font-mono text-xs ${statusColour(d.status)}`}>{d.status}</span>
42 </div>
43 <p className="mt-1 font-mono text-xs text-[var(--color-text-subtle)]">
44 created {new Date(d.createdAt).toISOString().replace('T', ' ').slice(0, 19)}
45 {d.finishedAt
46 ? ` · finished ${new Date(d.finishedAt).toISOString().replace('T', ' ').slice(11, 19)}`
47 : null}
48 </p>
49 <DiffSummary summary={d.schemaDiffSummary} functionCount={d.functionCount} />
50 {d.errorCode ? (
51 <p className="mt-2 font-mono text-xs text-red-400">
52 {d.errorCode}: {d.errorMessage}
53 </p>
54 ) : null}
55 </li>
56 ))}
57 </ul>
58 );
59}
60
61function DiffSummary({
62 summary,
63 functionCount,
64}: {
65 summary: Record<string, number> | null;
66 functionCount: string | null;
67}) {
68 const parts: string[] = [];
69 if (summary) {
70 if (summary.create_table) parts.push(`${summary.create_table} +table`);
71 if (summary.drop_table) parts.push(`${summary.drop_table} -table`);
72 if (summary.add_column) parts.push(`${summary.add_column} +col`);
73 if (summary.drop_column) parts.push(`${summary.drop_column} -col`);
74 }
75 if (functionCount && functionCount !== '0') parts.push(`${functionCount} fn`);
76 if (parts.length === 0) return null;
77 return (
78 <p className="mt-2 font-mono text-xs text-[var(--color-text-muted)]">{parts.join(' · ')}</p>
79 );
80}
81
82function statusColour(status: Deployment['status']): string {
83 switch (status) {
84 case 'succeeded':
85 return 'text-[var(--color-primary)]';
86 case 'failed':
87 return 'text-red-400';
88 case 'running':
89 case 'pending':
90 return 'text-[var(--color-text-muted)]';
91 case 'cancelled':
92 return 'text-[var(--color-text-subtle)]';
93 }
94}