invoke-panel.tsx102 lines · main
1'use client';
2
3import { useState, useTransition } from 'react';
4
5type Result =
6 | { ok: true; value: unknown; durationMs: number; deploymentId: string }
7 | {
8 ok: false;
9 code: string;
10 message: string;
11 durationMs: number;
12 deploymentId?: string;
13 };
14
15interface Props {
16 projectId: string;
17 functionName: string;
18}
19
20export function InvokePanel({ projectId, functionName }: Props) {
21 const [args, setArgs] = useState('{}');
22 const [result, setResult] = useState<Result | null>(null);
23 const [error, setError] = useState<string | null>(null);
24 const [pending, startTransition] = useTransition();
25
26 function run() {
27 setError(null);
28 let parsed: unknown = null;
29 try {
30 if (args.trim().length > 0) parsed = JSON.parse(args);
31 } catch {
32 setError('args must be valid json');
33 return;
34 }
35
36 startTransition(async () => {
37 const res = await fetch(`/api/v1/projects/${projectId}/functions/${functionName}`, {
38 method: 'POST',
39 headers: { 'content-type': 'application/json' },
40 credentials: 'include',
41 body: JSON.stringify(parsed),
42 });
43 const body = (await res.json()) as Result | { code: string; message: string };
44 if ('ok' in body) {
45 setResult(body);
46 } else {
47 setResult({ ok: false, code: body.code, message: body.message, durationMs: 0 });
48 }
49 });
50 }
51
52 return (
53 <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-5">
54 <div className="flex items-center justify-between">
55 <p className="font-mono text-sm text-[var(--color-text)]">{functionName}</p>
56 <button
57 type="button"
58 onClick={run}
59 disabled={pending}
60 className="rounded-md bg-[var(--color-primary)] px-3 py-1.5 font-mono text-xs font-medium text-[var(--color-text-inverse)] transition hover:bg-[var(--color-primary-hover)] disabled:opacity-50"
61 >
62 {pending ? 'invoking...' : 'invoke'}
63 </button>
64 </div>
65
66 <div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
67 <label className="flex flex-col gap-2">
68 <span className="font-mono text-xs text-[var(--color-text-muted)]">args (json)</span>
69 <textarea
70 value={args}
71 onChange={(e) => setArgs(e.currentTarget.value)}
72 spellCheck={false}
73 rows={8}
74 className="resize-none rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] p-3 font-mono text-xs text-[var(--color-text)] outline-none focus:border-[var(--color-primary)]"
75 />
76 {error ? (
77 <p role="alert" className="font-mono text-xs text-red-400">
78 {error}
79 </p>
80 ) : null}
81 </label>
82
83 <div className="flex flex-col gap-2">
84 <span className="font-mono text-xs text-[var(--color-text-muted)]">
85 result
86 {result ? (
87 <span className="ml-2 text-[var(--color-text-subtle)]">
88 {result.durationMs}ms ·{' '}
89 <span className={result.ok ? 'text-[var(--color-primary)]' : 'text-red-400'}>
90 {result.ok ? 'ok' : result.code}
91 </span>
92 </span>
93 ) : null}
94 </span>
95 <pre className="h-full min-h-[12rem] overflow-auto rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] p-3 font-mono text-xs text-[var(--color-text)]">
96 {result ? JSON.stringify(result.ok ? result.value : result.message, null, 2) : '—'}
97 </pre>
98 </div>
99 </div>
100 </div>
101 );
102}