page.tsx104 lines · main
1import Link from 'next/link';
2
3import { apiJson } from '../../../../../../lib/api';
4import { InvokePanel } from './invoke-panel';
5
6interface Deployment {
7 id: string;
8 status: 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
9 createdAt: string;
10 functionNames: string[] | null;
11}
12
13interface FunctionStats {
14 count: number;
15 errCount: number;
16 p50Ms: number;
17 p99Ms: number;
18 sinceHours: number;
19}
20
21export const dynamic = 'force-dynamic';
22
23export default async function FunctionsPage({ params }: { params: Promise<{ id: string }> }) {
24 const { id } = await params;
25 const { deployments } = await apiJson<{ deployments: Deployment[] }>(
26 `/v1/projects/${id}/deployments?limit=1`,
27 );
28
29 const current = deployments[0];
30 const live = current && current.status !== 'failed' && current.status !== 'cancelled';
31 const names = live ? (current.functionNames ?? []) : [];
32
33 if (!live || names.length === 0) {
34 return (
35 <p className="rounded-md border border-dashed border-[var(--color-border)] p-10 text-center font-mono text-sm text-[var(--color-text-muted)]">
36 no live functions. add files under <code>briven/functions/</code> and run{' '}
37 <code className="text-[var(--color-text)]">briven deploy</code>.
38 </p>
39 );
40 }
41
42 const stats = await Promise.all(
43 names.map(async (name) => {
44 const s = await apiJson<FunctionStats>(
45 `/v1/projects/${id}/function-stats?function=${encodeURIComponent(name)}`,
46 ).catch(() => null);
47 return [name, s] as const;
48 }),
49 );
50 const statsByName = new Map(stats);
51
52 return (
53 <div className="flex flex-col gap-6">
54 <header>
55 <h2 className="font-mono text-sm text-[var(--color-text)]">functions</h2>
56 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
57 served from deployment <code className="text-[var(--color-text)]">{current.id}</code> ·{' '}
58 {names.length} function{names.length === 1 ? '' : 's'} · status{' '}
59 <span className="text-[var(--color-text)]">{current.status}</span>
60 </p>
61 </header>
62
63 <ul className="flex flex-col gap-4">
64 {names.map((name) => {
65 const s = statsByName.get(name) ?? null;
66 const hasInvokes = s && s.count > 0;
67 return (
68 <li key={name} className="flex flex-col gap-2">
69 {hasInvokes ? (
70 <div className="flex flex-wrap items-center gap-3 font-mono text-[10px] text-[var(--color-text-muted)]">
71 <span>
72 last 24h:{' '}
73 <span className="text-[var(--color-text)]">{s.count.toLocaleString()}</span>{' '}
74 invocations
75 </span>
76 {s.errCount > 0 ? (
77 <span className="text-red-400">
78 {s.errCount} err ({Math.round((100 * s.errCount) / s.count)}%)
79 </span>
80 ) : (
81 <span className="text-[var(--color-text-subtle)]">0 errors</span>
82 )}
83 <span>
84 p50 <span className="text-[var(--color-text)]">{s.p50Ms}ms</span>
85 </span>
86 <span>
87 p99 <span className="text-[var(--color-text)]">{s.p99Ms}ms</span>
88 </span>
89 <Link
90 href={`/dashboard/projects/${id}/logs?function=${encodeURIComponent(name)}`}
91 className="ml-auto text-[var(--color-text-link)] hover:underline"
92 >
93 logs →
94 </Link>
95 </div>
96 ) : null}
97 <InvokePanel projectId={id} functionName={name} />
98 </li>
99 );
100 })}
101 </ul>
102 </div>
103 );
104}