launchbase 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,478 @@
1
+ import { useState } from 'react'
2
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
+ import {
4
+ Code, Plus, Play, Trash2, Zap,
5
+ Search, Clock, Activity, RefreshCw
6
+ } from 'lucide-react'
7
+ import { useToast } from '@/components/Toast'
8
+
9
+ interface EdgeFunction {
10
+ id: string
11
+ name: string
12
+ slug: string
13
+ runtime: string
14
+ sourceCode: string
15
+ status: string
16
+ deployedAt: string | null
17
+ deploymentUrl: string | null
18
+ createdAt: string
19
+ updatedAt: string
20
+ }
21
+
22
+ interface EdgeFunctionLog {
23
+ id: string
24
+ status: string
25
+ duration: number
26
+ input: Record<string, any> | null
27
+ output: Record<string, any> | null
28
+ error: string | null
29
+ triggeredBy: string | null
30
+ createdAt: string
31
+ }
32
+
33
+ const RUNTIMES = [
34
+ { value: 'nodejs18', label: 'Node.js 18' },
35
+ { value: 'nodejs20', label: 'Node.js 20' },
36
+ { value: 'python311', label: 'Python 3.11' },
37
+ { value: 'go121', label: 'Go 1.21' },
38
+ ]
39
+
40
+ export function EdgeFunctions() {
41
+ const queryClient = useQueryClient()
42
+ const toast = useToast()
43
+
44
+ const [searchQuery, setSearchQuery] = useState('')
45
+ const [showCreateModal, setShowCreateModal] = useState(false)
46
+ const [showEditModal, setShowEditModal] = useState(false)
47
+ const [showLogsModal, setShowLogsModal] = useState(false)
48
+ const [selectedFunction, setSelectedFunction] = useState<EdgeFunction | null>(null)
49
+ const [functionLogs, setFunctionLogs] = useState<EdgeFunctionLog[]>([])
50
+
51
+ const [newFunction, setNewFunction] = useState({
52
+ name: '',
53
+ slug: '',
54
+ runtime: 'nodejs18',
55
+ sourceCode: `export default async function handler(req, res) {
56
+ // Your code here
57
+ res.json({ message: 'Hello World' })
58
+ }`,
59
+ })
60
+
61
+ // Fetch functions
62
+ const { data: functionsData, isLoading } = useQuery({
63
+ queryKey: ['edge-functions'],
64
+ queryFn: async () => {
65
+ const res = await fetch('/api/functions')
66
+ if (!res.ok) throw new Error('Failed to fetch functions')
67
+ return res.json()
68
+ }
69
+ })
70
+
71
+ const functions = functionsData?.functions || []
72
+ const stats = functionsData?.stats || { total: 0, deployed: 0, draft: 0, invocations: 0 }
73
+
74
+ // Create function mutation
75
+ const createMutation = useMutation({
76
+ mutationFn: async () => {
77
+ const res = await fetch('/api/functions', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify(newFunction)
81
+ })
82
+ if (!res.ok) {
83
+ const error = await res.json()
84
+ throw new Error(error.message || 'Failed to create function')
85
+ }
86
+ return res.json()
87
+ },
88
+ onSuccess: () => {
89
+ queryClient.invalidateQueries({ queryKey: ['edge-functions'] })
90
+ setShowCreateModal(false)
91
+ setNewFunction({
92
+ name: '',
93
+ slug: '',
94
+ runtime: 'nodejs18',
95
+ sourceCode: `export default async function handler(req, res) {
96
+ // Your code here
97
+ res.json({ message: 'Hello World' })
98
+ }`,
99
+ })
100
+ toast.success('Function created successfully')
101
+ },
102
+ onError: (error: Error) => {
103
+ toast.error(error.message)
104
+ }
105
+ })
106
+
107
+ // Deploy function mutation
108
+ const deployMutation = useMutation({
109
+ mutationFn: async (functionId: string) => {
110
+ const res = await fetch(`/api/functions/${functionId}/deploy`, {
111
+ method: 'POST'
112
+ })
113
+ if (!res.ok) {
114
+ const error = await res.json()
115
+ throw new Error(error.message || 'Failed to deploy function')
116
+ }
117
+ return res.json()
118
+ },
119
+ onSuccess: () => {
120
+ queryClient.invalidateQueries({ queryKey: ['edge-functions'] })
121
+ toast.success('Function deployed successfully')
122
+ },
123
+ onError: (error: Error) => {
124
+ toast.error(error.message)
125
+ }
126
+ })
127
+
128
+ // Delete function mutation
129
+ const deleteMutation = useMutation({
130
+ mutationFn: async (functionId: string) => {
131
+ const res = await fetch(`/api/functions/${functionId}`, {
132
+ method: 'DELETE'
133
+ })
134
+ if (!res.ok) {
135
+ const error = await res.json()
136
+ throw new Error(error.message || 'Failed to delete function')
137
+ }
138
+ return res.json()
139
+ },
140
+ onSuccess: () => {
141
+ queryClient.invalidateQueries({ queryKey: ['edge-functions'] })
142
+ toast.success('Function deleted successfully')
143
+ },
144
+ onError: (error: Error) => {
145
+ toast.error(error.message)
146
+ }
147
+ })
148
+
149
+ // Fetch logs
150
+ const fetchLogs = async (functionId: string) => {
151
+ try {
152
+ const res = await fetch(`/api/functions/${functionId}/logs`)
153
+ if (res.ok) {
154
+ const data = await res.json()
155
+ setFunctionLogs(data.logs || [])
156
+ }
157
+ } catch (error) {
158
+ console.error('Failed to fetch logs:', error)
159
+ }
160
+ }
161
+
162
+ const handleCreate = () => {
163
+ if (!newFunction.name.trim()) {
164
+ toast.error('Function name is required')
165
+ return
166
+ }
167
+ const slug = newFunction.slug || newFunction.name.toLowerCase().replace(/[^a-z0-9]/g, '-')
168
+ createMutation.mutate()
169
+ }
170
+
171
+ const handleNameChange = (name: string) => {
172
+ const slug = name.toLowerCase().replace(/[^a-z0-9]/g, '-')
173
+ setNewFunction({ ...newFunction, name, slug })
174
+ }
175
+
176
+ const filteredFunctions = functions.filter((f: EdgeFunction) =>
177
+ f.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
178
+ f.slug.toLowerCase().includes(searchQuery.toLowerCase())
179
+ )
180
+
181
+ const getStatusColor = (status: string) => {
182
+ switch (status) {
183
+ case 'deployed': return 'bg-green-100 text-green-700'
184
+ case 'draft': return 'bg-yellow-100 text-yellow-700'
185
+ case 'failed': return 'bg-red-100 text-red-700'
186
+ default: return 'bg-gray-100 text-gray-700'
187
+ }
188
+ }
189
+
190
+ return (
191
+ <div>
192
+ <div className="flex items-center justify-between mb-6">
193
+ <div>
194
+ <h1 className="text-2xl font-bold text-gray-900">Edge Functions</h1>
195
+ <p className="text-gray-500 mt-1">Serverless functions for your project</p>
196
+ </div>
197
+ <button
198
+ onClick={() => setShowCreateModal(true)}
199
+ className="px-4 py-2 bg-primary text-white rounded-lg font-medium hover:bg-primary-600 flex items-center gap-2"
200
+ >
201
+ <Plus className="w-4 h-4" />
202
+ Create Function
203
+ </button>
204
+ </div>
205
+
206
+ {/* Stats */}
207
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
208
+ <div className="bg-white rounded-lg border p-4">
209
+ <div className="flex items-center gap-3">
210
+ <div className="p-2 bg-blue-100 rounded-lg">
211
+ <Code className="w-5 h-5 text-blue-600" />
212
+ </div>
213
+ <div>
214
+ <p className="text-2xl font-bold">{stats.total}</p>
215
+ <p className="text-sm text-gray-500">Functions</p>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ <div className="bg-white rounded-lg border p-4">
220
+ <div className="flex items-center gap-3">
221
+ <div className="p-2 bg-green-100 rounded-lg">
222
+ <Play className="w-5 h-5 text-green-600" />
223
+ </div>
224
+ <div>
225
+ <p className="text-2xl font-bold">{stats.deployed}</p>
226
+ <p className="text-sm text-gray-500">Deployed</p>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ <div className="bg-white rounded-lg border p-4">
231
+ <div className="flex items-center gap-3">
232
+ <div className="p-2 bg-yellow-100 rounded-lg">
233
+ <Clock className="w-5 h-5 text-yellow-600" />
234
+ </div>
235
+ <div>
236
+ <p className="text-2xl font-bold">{stats.draft}</p>
237
+ <p className="text-sm text-gray-500">Draft</p>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ <div className="bg-white rounded-lg border p-4">
242
+ <div className="flex items-center gap-3">
243
+ <div className="p-2 bg-purple-100 rounded-lg">
244
+ <Activity className="w-5 h-5 text-purple-600" />
245
+ </div>
246
+ <div>
247
+ <p className="text-2xl font-bold">{stats.invocations.toLocaleString()}</p>
248
+ <p className="text-sm text-gray-500">Invocations</p>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ {/* Search */}
255
+ <div className="mb-6">
256
+ <div className="relative max-w-md">
257
+ <Search className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
258
+ <input
259
+ type="text"
260
+ placeholder="Search functions..."
261
+ value={searchQuery}
262
+ onChange={(e) => setSearchQuery(e.target.value)}
263
+ className="w-full pl-10 pr-4 py-2 border rounded-lg"
264
+ />
265
+ </div>
266
+ </div>
267
+
268
+ {/* Functions List */}
269
+ <div className="bg-white rounded-lg border overflow-hidden">
270
+ {isLoading ? (
271
+ <div className="p-8 text-center text-gray-500">Loading functions...</div>
272
+ ) : filteredFunctions.length === 0 ? (
273
+ <div className="p-8 text-center">
274
+ <Zap className="w-12 h-12 text-gray-300 mx-auto mb-4" />
275
+ <p className="text-gray-500">No functions created yet</p>
276
+ <button
277
+ onClick={() => setShowCreateModal(true)}
278
+ className="mt-4 text-primary hover:underline"
279
+ >
280
+ Create your first function
281
+ </button>
282
+ </div>
283
+ ) : (
284
+ <table className="w-full">
285
+ <thead className="bg-gray-50 border-b">
286
+ <tr>
287
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Name</th>
288
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Status</th>
289
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Runtime</th>
290
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-500">Last Deployed</th>
291
+ <th className="text-right px-4 py-3 text-sm font-medium text-gray-500">Actions</th>
292
+ </tr>
293
+ </thead>
294
+ <tbody className="divide-y">
295
+ {filteredFunctions.map((func: EdgeFunction) => (
296
+ <tr key={func.id} className="hover:bg-gray-50">
297
+ <td className="px-4 py-3">
298
+ <div className="flex items-center gap-3">
299
+ <div className="w-8 h-8 bg-primary-100 rounded flex items-center justify-center">
300
+ <Code className="w-4 h-4 text-primary-600" />
301
+ </div>
302
+ <div>
303
+ <p className="font-medium text-gray-900">{func.name}</p>
304
+ <p className="text-sm text-gray-500">/{func.slug}</p>
305
+ </div>
306
+ </div>
307
+ </td>
308
+ <td className="px-4 py-3">
309
+ <span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(func.status)}`}>
310
+ {func.status}
311
+ </span>
312
+ </td>
313
+ <td className="px-4 py-3 text-sm text-gray-500">{func.runtime}</td>
314
+ <td className="px-4 py-3 text-sm text-gray-500">
315
+ {func.deployedAt ? new Date(func.deployedAt).toLocaleDateString() : 'Never'}
316
+ </td>
317
+ <td className="px-4 py-3">
318
+ <div className="flex items-center justify-end gap-2">
319
+ <button
320
+ onClick={() => deployMutation.mutate(func.id)}
321
+ className="p-1 hover:bg-green-100 rounded"
322
+ title="Deploy"
323
+ disabled={deployMutation.isPending}
324
+ >
325
+ <Play className="w-4 h-4 text-gray-400 hover:text-green-500" />
326
+ </button>
327
+ <button
328
+ onClick={() => {
329
+ setSelectedFunction(func)
330
+ fetchLogs(func.id)
331
+ setShowLogsModal(true)
332
+ }}
333
+ className="p-1 hover:bg-blue-100 rounded"
334
+ title="View Logs"
335
+ >
336
+ <Activity className="w-4 h-4 text-gray-400 hover:text-blue-500" />
337
+ </button>
338
+ <button
339
+ onClick={() => {
340
+ if (confirm('Are you sure you want to delete this function?')) {
341
+ deleteMutation.mutate(func.id)
342
+ }
343
+ }}
344
+ className="p-1 hover:bg-red-100 rounded"
345
+ title="Delete"
346
+ >
347
+ <Trash2 className="w-4 h-4 text-gray-400 hover:text-red-500" />
348
+ </button>
349
+ </div>
350
+ </td>
351
+ </tr>
352
+ ))}
353
+ </tbody>
354
+ </table>
355
+ )}
356
+ </div>
357
+
358
+ {/* Create Function Modal */}
359
+ {showCreateModal && (
360
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
361
+ <div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
362
+ <h2 className="text-xl font-bold mb-4">Create Edge Function</h2>
363
+ <div className="space-y-4">
364
+ <div>
365
+ <label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
366
+ <input
367
+ type="text"
368
+ value={newFunction.name}
369
+ onChange={(e) => handleNameChange(e.target.value)}
370
+ className="w-full px-3 py-2 border rounded-lg"
371
+ placeholder="my-function"
372
+ />
373
+ </div>
374
+ <div>
375
+ <label className="block text-sm font-medium text-gray-700 mb-1">Slug</label>
376
+ <input
377
+ type="text"
378
+ value={newFunction.slug}
379
+ onChange={(e) => setNewFunction({ ...newFunction, slug: e.target.value })}
380
+ className="w-full px-3 py-2 border rounded-lg"
381
+ placeholder="my-function"
382
+ />
383
+ </div>
384
+ <div>
385
+ <label className="block text-sm font-medium text-gray-700 mb-1">Runtime</label>
386
+ <select
387
+ value={newFunction.runtime}
388
+ onChange={(e) => setNewFunction({ ...newFunction, runtime: e.target.value })}
389
+ className="w-full px-3 py-2 border rounded-lg"
390
+ >
391
+ {RUNTIMES.map(r => (
392
+ <option key={r.value} value={r.value}>{r.label}</option>
393
+ ))}
394
+ </select>
395
+ </div>
396
+ <div>
397
+ <label className="block text-sm font-medium text-gray-700 mb-1">Code</label>
398
+ <textarea
399
+ value={newFunction.sourceCode}
400
+ onChange={(e) => setNewFunction({ ...newFunction, sourceCode: e.target.value })}
401
+ className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
402
+ rows={12}
403
+ />
404
+ </div>
405
+ </div>
406
+ <div className="flex justify-end gap-3 mt-6">
407
+ <button
408
+ onClick={() => setShowCreateModal(false)}
409
+ className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
410
+ >
411
+ Cancel
412
+ </button>
413
+ <button
414
+ onClick={handleCreate}
415
+ disabled={createMutation.isPending}
416
+ className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-600 disabled:opacity-50"
417
+ >
418
+ {createMutation.isPending ? 'Creating...' : 'Create Function'}
419
+ </button>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ )}
424
+
425
+ {/* Logs Modal */}
426
+ {showLogsModal && selectedFunction && (
427
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
428
+ <div className="bg-white rounded-lg p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
429
+ <div className="flex items-center justify-between mb-4">
430
+ <h2 className="text-xl font-bold">Logs - {selectedFunction.name}</h2>
431
+ <button
432
+ onClick={() => fetchLogs(selectedFunction.id)}
433
+ className="p-2 hover:bg-gray-100 rounded"
434
+ >
435
+ <RefreshCw className="w-4 h-4" />
436
+ </button>
437
+ </div>
438
+ {functionLogs.length === 0 ? (
439
+ <p className="text-gray-500 text-center py-8">No logs yet</p>
440
+ ) : (
441
+ <div className="space-y-2">
442
+ {functionLogs.map((log) => (
443
+ <div key={log.id} className="p-3 bg-gray-50 rounded-lg">
444
+ <div className="flex items-center justify-between mb-2">
445
+ <span className={`px-2 py-1 text-xs rounded ${
446
+ log.status === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
447
+ }`}>
448
+ {log.status}
449
+ </span>
450
+ <span className="text-xs text-gray-500">
451
+ {new Date(log.createdAt).toLocaleString()}
452
+ </span>
453
+ </div>
454
+ <p className="text-sm text-gray-600">Duration: {log.duration}ms</p>
455
+ {log.error && (
456
+ <p className="text-sm text-red-500 mt-1">{log.error}</p>
457
+ )}
458
+ </div>
459
+ ))}
460
+ </div>
461
+ )}
462
+ <div className="flex justify-end mt-6">
463
+ <button
464
+ onClick={() => {
465
+ setShowLogsModal(false)
466
+ setFunctionLogs([])
467
+ }}
468
+ className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
469
+ >
470
+ Close
471
+ </button>
472
+ </div>
473
+ </div>
474
+ </div>
475
+ )}
476
+ </div>
477
+ )
478
+ }